ault null. * @param string $username Username for authentication. * @param string $password Password for authentication. * @return WP_User|WP_Error WP_User on success, WP_Error on failure. */ function wp_authenticate_username_password( $user, $username, $password ) { if ( $user instanceof WP_User ) { return $user; } if ( empty( $username ) || empty( $password ) ) { if ( is_wp_error( $user ) ) { return $user; } $error = new WP_Error(); if ( empty( $username ) ) { $error->add( 'empty_username', __( 'Error: The username field is empty.' ) ); } if ( empty( $password ) ) { $error->add( 'empty_password', __( 'Error: The password field is empty.' ) ); } return $error; } $user = get_user_by( 'login', $username ); if ( ! $user ) { return new WP_Error( 'invalid_username', sprintf( /* translators: %s: User name. */ __( 'Error: The username %s is not registered on this site. If you are unsure of your username, try your email address instead.' ), $username ) ); } /** * Filters whether the given user can be authenticated with the provided password. * * @since 2.5.0 * * @param WP_User|WP_Error $user WP_User or WP_Error object if a previous * callback failed authentication. * @param string $password Password to check against the user. */ $user = apply_filters( 'wp_authenticate_user', $user, $password ); if ( is_wp_error( $user ) ) { return $user; } if ( ! wp_check_password( $password, $user->user_pass, $user->ID ) ) { return new WP_Error( 'incorrect_password', sprintf( /* translators: %s: User name. */ __( 'Error: The password you entered for the username %s is incorrect.' ), '' . $username . '' ) . ' ' . __( 'Lost your password?' ) . '' ); } return $user; } /** * Authenticates a user using the email and password. * * @since 4.5.0 * * @param WP_User|WP_Error|null $user WP_User or WP_Error object if a previous * callback failed authentication. * @param string $email Email address for authentication. * @param string $password Password for authentication. * @return WP_User|WP_Error WP_User on success, WP_Error on failure. */ function wp_authenticate_email_password( $user, $email, $password ) { if ( $user instanceof WP_User ) { return $user; } if ( empty( $email ) || empty( $password ) ) { if ( is_wp_error( $user ) ) { return $user; } $error = new WP_Error(); if ( empty( $email ) ) { // Uses 'empty_username' for back-compat with wp_signon(). $error->add( 'empty_username', __( 'Error: The email field is empty.' ) ); } if ( empty( $password ) ) { $error->add( 'empty_password', __( 'Error: The password field is empty.' ) ); } return $error; } if ( ! is_email( $email ) ) { return $user; } $user = get_user_by( 'email', $email ); if ( ! $user ) { return new WP_Error( 'invalid_email', __( 'Unknown email address. Check again or try your username.' ) ); } /** This filter is documented in wp-includes/user.php */ $user = apply_filters( 'wp_authenticate_user', $user, $password ); if ( is_wp_error( $user ) ) { return $user; } if ( ! wp_check_password( $password, $user->user_pass, $user->ID ) ) { return new WP_Error( 'incorrect_password', sprintf( /* translators: %s: Email address. */ __( 'Error: The password you entered for the email address %s is incorrect.' ), '' . $email . '' ) . ' ' . __( 'Lost your password?' ) . '' ); } return $user; } /** * Authenticates the user using the WordPress auth cookie. * * @since 2.8.0 * * @global string $auth_secure_cookie * * @param WP_User|WP_Error|null $user WP_User or WP_Error object from a previous callback. Default null. * @param string $username Username. If not empty, cancels the cookie authentication. * @param string $password Password. If not empty, cancels the cookie authentication. * @return WP_User|WP_Error WP_User on success, WP_Error on failure. */ function wp_authenticate_cookie( $user, $username, $password ) { if ( $user instanceof WP_User ) { return $user; } if ( empty( $username ) && empty( $password ) ) { $user_id = wp_validate_auth_cookie(); if ( $user_id ) { return new WP_User( $user_id ); } global $auth_secure_cookie; if ( $auth_secure_cookie ) { $auth_cookie = SECURE_AUTH_COOKIE; } else { $auth_cookie = AUTH_COOKIE; } if ( ! empty( $_COOKIE[ $auth_cookie ] ) ) { return new WP_Error( 'expired_session', __( 'Please log in again.' ) ); } // If the cookie is not set, be silent. } return $user; } /** * Authenticates the user using an application password. * * @since 5.6.0 * * @param WP_User|WP_Error|null $input_user WP_User or WP_Error object if a previous * callback failed authentication. * @param string $username Username for authentication. * @param string $password Password for authentication. * @return WP_User|WP_Error|null WP_User on success, WP_Error on failure, null if * null is passed in and this isn't an API request. */ function wp_authenticate_application_password( $input_user, $username, $password ) { if ( $input_user instanceof WP_User ) { return $input_user; } if ( ! WP_Application_Passwords::is_in_use() ) { return $input_user; } $is_api_request = ( ( defined( 'XMLRPC_REQUEST' ) && XMLRPC_REQUEST ) || ( defined( 'REST_REQUEST' ) && REST_REQUEST ) ); /** * Filters whether this is an API request that Application Passwords can be used on. * * By default, Application Passwords is available for the REST API and XML-RPC. * * @since 5.6.0 * * @param bool $is_api_request If this is an acceptable API request. */ $is_api_request = apply_filters( 'application_password_is_api_request', $is_api_request ); if ( ! $is_api_request ) { return $input_user; } $error = null; $user = get_user_by( 'login', $username ); if ( ! $user && is_email( $username ) ) { $user = get_user_by( 'email', $username ); } // If the login name is invalid, short circuit. if ( ! $user ) { if ( is_email( $username ) ) { $error = new WP_Error( 'invalid_email', __( 'Error: Unknown email address. Check again or try your username.' ) ); } else { $error = new WP_Error( 'invalid_username', __( 'Error: Unknown username. Check again or try your email address.' ) ); } } elseif ( ! wp_is_application_passwords_available() ) { $error = new WP_Error( 'application_passwords_disabled', __( 'Application passwords are not available.' ) ); } elseif ( ! wp_is_application_passwords_available_for_user( $user ) ) { $error = new WP_Error( 'application_passwords_disabled_for_user', __( 'Application passwords are not available for your account. Please contact the site administrator for assistance.' ) ); } if ( $error ) { /** * Fires when an application password failed to authenticate the user. * * @since 5.6.0 * * @param WP_Error $error The authentication error. */ do_action( 'application_password_failed_authentication', $error ); return $error; } /* * Strips out anything non-alphanumeric. This is so passwords can be used with * or without spaces to indicate the groupings for readability. * * Generated application passwords are exclusively alphanumeric. */ $password = preg_replace( '/[^a-z\d]/i', '', $password ); $hashed_passwords = WP_Application_Passwords::get_user_application_passwords( $user->ID ); foreach ( $hashed_passwords as $key => $item ) { if ( ! wp_check_password( $password, $item['password'], $user->ID ) ) { continue; } $error = new WP_Error(); /** * Fires when an application password has been successfully checked as valid. * * This allows for plugins to add additional constraints to prevent an application password from being used. * * @since 5.6.0 * * @param WP_Error $error The error object. * @param WP_User $user The user authenticating. * @param array $item The details about the application password. * @param string $password The raw supplied password. */ do_action( 'wp_authenticate_application_password_errors', $error, $user, $item, $password ); if ( is_wp_error( $error ) && $error->has_errors() ) { /** This action is documented in wp-includes/user.php */ do_action( 'application_password_failed_authentication', $error ); return $error; } WP_Application_Passwords::record_application_password_usage( $user->ID, $item['uuid'] ); /** * Fires after an application password was used for authentication. * * @since 5.6.0 * * @param WP_User $user The user who was authenticated. * @param array $item The application password used. */ do_action( 'application_password_did_authenticate', $user, $item ); return $user; } $error = new WP_Error( 'incorrect_password', __( 'The provided password is an invalid application password.' ) ); /** This action is documented in wp-includes/user.php */ do_action( 'application_password_failed_authentication', $error ); return $error; } /** * Validates the application password credentials passed via Basic Authentication. * * @since 5.6.0 * * @param int|false $input_user User ID if one has been determined, false otherwise. * @return int|false The authenticated user ID if successful, false otherwise. */ function wp_validate_application_password( $input_user ) { // Don't authenticate twice. if ( ! empty( $input_user ) ) { return $input_user; } if ( ! wp_is_application_passwords_available() ) { return $input_user; } // Both $_SERVER['PHP_AUTH_USER'] and $_SERVER['PHP_AUTH_PW'] must be set in order to attempt authentication. if ( ! isset( $_SERVER['PHP_AUTH_USER'], $_SERVER['PHP_AUTH_PW'] ) ) { return $input_user; } $authenticated = wp_authenticate_application_password( null, $_SERVER['PHP_AUTH_USER'], $_SERVER['PHP_AUTH_PW'] ); if ( $authenticated instanceof WP_User ) { return $authenticated->ID; } // If it wasn't a user what got returned, just pass on what we had received originally. return $input_user; } /** * For Multisite blogs, checks if the authenticated user has been marked as a * spammer, or if the user's primary blog has been marked as spam. * * @since 3.7.0 * * @param WP_User|WP_Error|null $user WP_User or WP_Error object from a previous callback. Default null. * @return WP_User|WP_Error WP_User on success, WP_Error if the user is considered a spammer. */ function wp_authenticate_spam_check( $user ) { if ( $user instanceof WP_User && is_multisite() ) { /** * Filters whether the user has been marked as a spammer. * * @since 3.7.0 * * @param bool $spammed Whether the user is considered a spammer. * @param WP_User $user User to check against. */ $spammed = apply_filters( 'check_is_user_spammed', is_user_spammy( $user ), $user ); if ( $spammed ) { return new WP_Error( 'spammer_account', __( 'Error: Your account has been marked as a spammer.' ) ); } } return $user; } /** * Validates the logged-in cookie. * * Checks the logged-in cookie if the previous auth cookie could not be * validated and parsed. * * This is a callback for the {@see 'determine_current_user'} filter, rather than API. * * @since 3.9.0 * * @param int|false $user_id The user ID (or false) as received from * the `determine_current_user` filter. * @return int|false User ID if validated, false otherwise. If a user ID from * an earlier filter callback is received, that value is returned. */ function wp_validate_logged_in_cookie( $user_id ) { if ( $user_id ) { return $user_id; } if ( is_blog_admin() || is_network_admin() || empty( $_COOKIE[ LOGGED_IN_COOKIE ] ) ) { return false; } return wp_validate_auth_cookie( $_COOKIE[ LOGGED_IN_COOKIE ], 'logged_in' ); } /** * Gets the number of posts a user has written. * * @since 3.0.0 * @since 4.1.0 Added `$post_type` argument. * @since 4.3.0 Added `$public_only` argument. Added the ability to pass an array * of post types to `$post_type`. * * @global wpdb $wpdb WordPress database abstraction object. * * @param int $userid User ID. * @param array|string $post_type Optional. Single post type or array of post types to count the number of posts for. Default 'post'. * @param bool $public_only Optional. Whether to only return counts for public posts. Default false. * @return string Number of posts the user has written in this post type. */ function count_user_posts( $userid, $post_type = 'post', $public_only = false ) { global $wpdb; $where = get_posts_by_author_sql( $post_type, true, $userid, $public_only ); $count = $wpdb->get_var( "SELECT COUNT(*) FROM $wpdb->posts $where" ); /** * Filters the number of posts a user has written. * * @since 2.7.0 * @since 4.1.0 Added `$post_type` argument. * @since 4.3.1 Added `$public_only` argument. * * @param int $count The user's post count. * @param int $userid User ID. * @param string|array $post_type Single post type or array of post types to count the number of posts for. * @param bool $public_only Whether to limit counted posts to public posts. */ return apply_filters( 'get_usernumposts', $count, $userid, $post_type, $public_only ); } /** * Gets the number of posts written by a list of users. * * @since 3.0.0 * * @global wpdb $wpdb WordPress database abstraction object. * * @param int[] $users Array of user IDs. * @param string|string[] $post_type Optional. Single post type or array of post types to check. Defaults to 'post'. * @param bool $public_only Optional. Only return counts for public posts. Defaults to false. * @return string[] Amount of posts each user has written, as strings, keyed by user ID. */ function count_many_users_posts( $users, $post_type = 'post', $public_only = false ) { global $wpdb; $count = array(); if ( empty( $users ) || ! is_array( $users ) ) { return $count; } $userlist = implode( ',', array_map( 'absint', $users ) ); $where = get_posts_by_author_sql( $post_type, true, null, $public_only ); $result = $wpdb->get_results( "SELECT post_author, COUNT(*) FROM $wpdb->posts $where AND post_author IN ($userlist) GROUP BY post_author", ARRAY_N ); foreach ( $result as $row ) { $count[ $row[0] ] = $row[1]; } foreach ( $users as $id ) { if ( ! isset( $count[ $id ] ) ) { $count[ $id ] = 0; } } return $count; } // // User option functions. // /** * Gets the current user's ID. * * @since MU (3.0.0) * * @return int The current user's ID, or 0 if no user is logged in. */ function get_current_user_id() { if ( ! function_exists( 'wp_get_current_user' ) ) { return 0; } $user = wp_get_current_user(); return ( isset( $user->ID ) ? (int) $user->ID : 0 ); } /** * Retrieves user option that can be either per Site or per Network. * * If the user ID is not given, then the current user will be used instead. If * the user ID is given, then the user data will be retrieved. The filter for * the result, will also pass the original option name and finally the user data * object as the third parameter. * * The option will first check for the per site name and then the per Network name. * * @since 2.0.0 * * @global wpdb $wpdb WordPress database abstraction object. * * @param string $option User option name. * @param int $user Optional. User ID. * @param string $deprecated Use get_option() to check for an option in the options table. * @return mixed User option value on success, false on failure. */ function get_user_option( $option, $user = 0, $deprecated = '' ) { global $wpdb; if ( ! empty( $deprecated ) ) { _deprecated_argument( __FUNCTION__, '3.0.0' ); } if ( empty( $user ) ) { $user = get_current_user_id(); } $user = get_userdata( $user ); if ( ! $user ) { return false; } $prefix = $wpdb->get_blog_prefix(); if ( $user->has_prop( $prefix . $option ) ) { // Blog-specific. $result = $user->get( $prefix . $option ); } elseif ( $user->has_prop( $option ) ) { // User-specific and cross-blog. $result = $user->get( $option ); } else { $result = false; } /** * Filters a specific user option value. * * The dynamic portion of the hook name, `$option`, refers to the user option name. * * @since 2.5.0 * * @param mixed $result Value for the user's option. * @param string $option Name of the option being retrieved. * @param WP_User $user WP_User object of the user whose option is being retrieved. */ return apply_filters( "get_user_option_{$option}", $result, $option, $user ); } /** * Updates user option with global blog capability. * * User options are just like user metadata except that they have support for * global blog options. If the 'is_global' parameter is false, which it is by default, * it will prepend the WordPress table prefix to the option name. * * Deletes the user option if $newvalue is empty. * * @since 2.0.0 * * @global wpdb $wpdb WordPress database abstraction object. * * @param int $user_id User ID. * @param string $option_name User option name. * @param mixed $newvalue User option value. * @param bool $is_global Optional. Whether option name is global or blog specific. * Default false (blog specific). * @return int|bool User meta ID if the option didn't exist, true on successful update, * false on failure. */ function update_user_option( $user_id, $option_name, $newvalue, $is_global = false ) { global $wpdb; if ( ! $is_global ) { $option_name = $wpdb->get_blog_prefix() . $option_name; } return update_user_meta( $user_id, $option_name, $newvalue ); } /** * Deletes user option with global blog capability. * * User options are just like user metadata except that they have support for * global blog options. If the 'is_global' parameter is false, which it is by default, * it will prepend the WordPress table prefix to the option name. * * @since 3.0.0 * * @global wpdb $wpdb WordPress database abstraction object. * * @param int $user_id User ID * @param string $option_name User option name. * @param bool $is_global Optional. Whether option name is global or blog specific. * Default false (blog specific). * @return bool True on success, false on failure. */ function delete_user_option( $user_id, $option_name, $is_global = false ) { global $wpdb; if ( ! $is_global ) { $option_name = $wpdb->get_blog_prefix() . $option_name; } return delete_user_meta( $user_id, $option_name ); } /** * Retrieves list of users matching criteria. * * @since 3.1.0 * * @see WP_User_Query * * @param array $args Optional. Arguments to retrieve users. See WP_User_Query::prepare_query() * for more information on accepted arguments. * @return array List of users. */ function get_users( $args = array() ) { $args = wp_parse_args( $args ); $args['count_total'] = false; $user_search = new WP_User_Query( $args ); return (array) $user_search->get_results(); } /** * Lists all the users of the site, with several options available. * * @since 5.9.0 * * @param string|array $args { * Optional. Array or string of default arguments. * * @type string $orderby How to sort the users. Accepts 'nicename', 'email', 'url', 'registered', * 'user_nicename', 'user_email', 'user_url', 'user_registered', 'name', * 'display_name', 'post_count', 'ID', 'meta_value', 'user_login'. Default 'name'. * @type string $order Sorting direction for $orderby. Accepts 'ASC', 'DESC'. Default 'ASC'. * @type int $number Maximum users to return or display. Default empty (all users). * @type bool $exclude_admin Whether to exclude the 'admin' account, if it exists. Default false. * @type bool $show_fullname Whether to show the user's full name. Default false. * @type string $feed If not empty, show a link to the user's feed and use this text as the alt * parameter of the link. Default empty. * @type string $feed_image If not empty, show a link to the user's feed and use this image URL as * clickable anchor. Default empty. * @type string $feed_type The feed type to link to, such as 'rss2'. Defaults to default feed type. * @type bool $echo Whether to output the result or instead return it. Default true. * @type string $style If 'list', each user is wrapped in an `
$network_id
'
),
'6.0.0'
);
}
return (int) get_network_option( $network_id, 'user_count', -1 );
}
/**
* Updates the total count of users on the site if live user counting is enabled.
*
* @since 6.0.0
*
* @param int|null $network_id ID of the network. Defaults to the current network.
* @return bool Whether the update was successful.
*/
function wp_maybe_update_user_counts( $network_id = null ) {
if ( ! is_multisite() && null !== $network_id ) {
_doing_it_wrong(
__FUNCTION__,
sprintf(
/* translators: %s: $network_id */
__( 'Unable to pass %s if not using multisite.' ),
'$network_id
'
),
'6.0.0'
);
}
$is_small_network = ! wp_is_large_user_count( $network_id );
/** This filter is documented in wp-includes/ms-functions.php */
if ( ! apply_filters( 'enable_live_network_counts', $is_small_network, 'users' ) ) {
return false;
}
return wp_update_user_counts( $network_id );
}
/**
* Updates the total count of users on the site.
*
* @global wpdb $wpdb WordPress database abstraction object.
* @since 6.0.0
*
* @param int|null $network_id ID of the network. Defaults to the current network.
* @return bool Whether the update was successful.
*/
function wp_update_user_counts( $network_id = null ) {
global $wpdb;
if ( ! is_multisite() && null !== $network_id ) {
_doing_it_wrong(
__FUNCTION__,
sprintf(
/* translators: %s: $network_id */
__( 'Unable to pass %s if not using multisite.' ),
'$network_id
'
),
'6.0.0'
);
}
$query = "SELECT COUNT(ID) as c FROM $wpdb->users";
if ( is_multisite() ) {
$query .= " WHERE spam = '0' AND deleted = '0'";
}
$count = $wpdb->get_var( $query );
return update_network_option( $network_id, 'user_count', $count );
}
/**
* Schedules a recurring recalculation of the total count of users.
*
* @since 6.0.0
*/
function wp_schedule_update_user_counts() {
if ( ! is_main_site() ) {
return;
}
if ( ! wp_next_scheduled( 'wp_update_user_counts' ) && ! wp_installing() ) {
wp_schedule_event( time(), 'twicedaily', 'wp_update_user_counts' );
}
}
/**
* Determines whether the site has a large number of users.
*
* The default criteria for a large site is more than 10,000 users.
*
* @since 6.0.0
*
* @param int|null $network_id ID of the network. Defaults to the current network.
* @return bool Whether the site has a large number of users.
*/
function wp_is_large_user_count( $network_id = null ) {
if ( ! is_multisite() && null !== $network_id ) {
_doing_it_wrong(
__FUNCTION__,
sprintf(
/* translators: %s: $network_id */
__( 'Unable to pass %s if not using multisite.' ),
'$network_id
'
),
'6.0.0'
);
}
$count = get_user_count( $network_id );
/**
* Filters whether the site is considered large, based on its number of users.
*
* @since 6.0.0
*
* @param bool $is_large_user_count Whether the site has a large number of users.
* @param int $count The total number of users.
* @param int|null $network_id ID of the network. `null` represents the current network.
*/
return apply_filters( 'wp_is_large_user_count', $count > 10000, $count, $network_id );
}
//
// Private helper functions.
//
/**
* Sets up global user vars.
*
* Used by wp_set_current_user() for back compat. Might be deprecated in the future.
*
* @since 2.0.4
*
* @global string $user_login The user username for logging in
* @global WP_User $userdata User data.
* @global int $user_level The level of the user
* @global int $user_ID The ID of the user
* @global string $user_email The email address of the user
* @global string $user_url The url in the user's profile
* @global string $user_identity The display name of the user
*
* @param int $for_user_id Optional. User ID to set up global data. Default 0.
*/
function setup_userdata( $for_user_id = 0 ) {
global $user_login, $userdata, $user_level, $user_ID, $user_email, $user_url, $user_identity;
if ( ! $for_user_id ) {
$for_user_id = get_current_user_id();
}
$user = get_userdata( $for_user_id );
if ( ! $user ) {
$user_ID = 0;
$user_level = 0;
$userdata = null;
$user_login = '';
$user_email = '';
$user_url = '';
$user_identity = '';
return;
}
$user_ID = (int) $user->ID;
$user_level = (int) $user->user_level;
$userdata = $user;
$user_login = $user->user_login;
$user_email = $user->user_email;
$user_url = $user->user_url;
$user_identity = $user->display_name;
}
/**
* Creates dropdown HTML content of users.
*
* The content can either be displayed, which it is by default or retrieved by
* setting the 'echo' argument. The 'include' and 'exclude' arguments do not
* need to be used; all users will be displayed in that case. Only one can be
* used, either 'include' or 'exclude', but not both.
*
* The available arguments are as follows:
*
* @since 2.3.0
* @since 4.5.0 Added the 'display_name_with_login' value for 'show'.
* @since 4.7.0 Added the `$role`, `$role__in`, and `$role__not_in` parameters.
*
* @param array|string $args {
* Optional. Array or string of arguments to generate a drop-down of users.
* See WP_User_Query::prepare_query() for additional available arguments.
*
* @type string $show_option_all Text to show as the drop-down default (all).
* Default empty.
* @type string $show_option_none Text to show as the drop-down default when no
* users were found. Default empty.
* @type int|string $option_none_value Value to use for $show_option_none when no users
* were found. Default -1.
* @type string $hide_if_only_one_author Whether to skip generating the drop-down
* if only one user was found. Default empty.
* @type string $orderby Field to order found users by. Accepts user fields.
* Default 'display_name'.
* @type string $order Whether to order users in ascending or descending
* order. Accepts 'ASC' (ascending) or 'DESC' (descending).
* Default 'ASC'.
* @type int[]|string $include Array or comma-separated list of user IDs to include.
* Default empty.
* @type int[]|string $exclude Array or comma-separated list of user IDs to exclude.
* Default empty.
* @type bool|int $multi Whether to skip the ID attribute on the 'select' element.
* Accepts 1|true or 0|false. Default 0|false.
* @type string $show User data to display. If the selected item is empty
* then the 'user_login' will be displayed in parentheses.
* Accepts any user field, or 'display_name_with_login' to show
* the display name with user_login in parentheses.
* Default 'display_name'.
* @type int|bool $echo Whether to echo or return the drop-down. Accepts 1|true (echo)
* or 0|false (return). Default 1|true.
* @type int $selected Which user ID should be selected. Default 0.
* @type bool $include_selected Whether to always include the selected user ID in the drop-
* down. Default false.
* @type string $name Name attribute of select element. Default 'user'.
* @type string $id ID attribute of the select element. Default is the value of $name.
* @type string $class Class attribute of the select element. Default empty.
* @type int $blog_id ID of blog (Multisite only). Default is ID of the current blog.
* @type string $who Which type of users to query. Accepts only an empty string or
* 'authors'. Default empty.
* @type string|array $role An array or a comma-separated list of role names that users must
* match to be included in results. Note that this is an inclusive
* list: users must match *each* role. Default empty.
* @type string[] $role__in An array of role names. Matched users must have at least one of
* these roles. Default empty array.
* @type string[] $role__not_in An array of role names to exclude. Users matching one or more of
* these roles will not be included in results. Default empty array.
* }
* @return string HTML dropdown list of users.
*/
function wp_dropdown_users( $args = '' ) {
$defaults = array(
'show_option_all' => '',
'show_option_none' => '',
'hide_if_only_one_author' => '',
'orderby' => 'display_name',
'order' => 'ASC',
'include' => '',
'exclude' => '',
'multi' => 0,
'show' => 'display_name',
'echo' => 1,
'selected' => 0,
'name' => 'user',
'class' => '',
'id' => '',
'blog_id' => get_current_blog_id(),
'who' => '',
'include_selected' => false,
'option_none_value' => -1,
'role' => '',
'role__in' => array(),
'role__not_in' => array(),
'capability' => '',
'capability__in' => array(),
'capability__not_in' => array(),
);
$defaults['selected'] = is_author() ? get_query_var( 'author' ) : 0;
$parsed_args = wp_parse_args( $args, $defaults );
$query_args = wp_array_slice_assoc(
$parsed_args,
array(
'blog_id',
'include',
'exclude',
'orderby',
'order',
'who',
'role',
'role__in',
'role__not_in',
'capability',
'capability__in',
'capability__not_in',
)
);
$fields = array( 'ID', 'user_login' );
$show = ! empty( $parsed_args['show'] ) ? $parsed_args['show'] : 'display_name';
if ( 'display_name_with_login' === $show ) {
$fields[] = 'display_name';
} else {
$fields[] = $show;
}
$query_args['fields'] = $fields;
$show_option_all = $parsed_args['show_option_all'];
$show_option_none = $parsed_args['show_option_none'];
$option_none_value = $parsed_args['option_none_value'];
/**
* Filters the query arguments for the list of users in the dropdown.
*
* @since 4.4.0
*
* @param array $query_args The query arguments for get_users().
* @param array $parsed_args The arguments passed to wp_dropdown_users() combined with the defaults.
*/
$query_args = apply_filters( 'wp_dropdown_users_args', $query_args, $parsed_args );
$users = get_users( $query_args );
$output = '';
if ( ! empty( $users ) && ( empty( $parsed_args['hide_if_only_one_author'] ) || count( $users ) > 1 ) ) {
$name = esc_attr( $parsed_args['name'] );
if ( $parsed_args['multi'] && ! $parsed_args['id'] ) {
$id = '';
} else {
$id = $parsed_args['id'] ? " id='" . esc_attr( $parsed_args['id'] ) . "'" : " id='$name'";
}
$output = "';
}
/**
* Filters the wp_dropdown_users() HTML output.
*
* @since 2.3.0
*
* @param string $output HTML output generated by wp_dropdown_users().
*/
$html = apply_filters( 'wp_dropdown_users', $output );
if ( $parsed_args['echo'] ) {
echo $html;
}
return $html;
}
/**
* Sanitizes user field based on context.
*
* Possible context values are: 'raw', 'edit', 'db', 'display', 'attribute' and 'js'. The
* 'display' context is used by default. 'attribute' and 'js' contexts are treated like 'display'
* when calling filters.
*
* @since 2.3.0
*
* @param string $field The user Object field name.
* @param mixed $value The user Object value.
* @param int $user_id User ID.
* @param string $context How to sanitize user fields. Looks for 'raw', 'edit', 'db', 'display',
* 'attribute' and 'js'.
* @return mixed Sanitized value.
*/
function sanitize_user_field( $field, $value, $user_id, $context ) {
$int_fields = array( 'ID' );
if ( in_array( $field, $int_fields, true ) ) {
$value = (int) $value;
}
if ( 'raw' === $context ) {
return $value;
}
if ( ! is_string( $value ) && ! is_numeric( $value ) ) {
return $value;
}
$prefixed = str_contains( $field, 'user_' );
if ( 'edit' === $context ) {
if ( $prefixed ) {
/** This filter is documented in wp-includes/post.php */
$value = apply_filters( "edit_{$field}", $value, $user_id );
} else {
/**
* Filters a user field value in the 'edit' context.
*
* The dynamic portion of the hook name, `$field`, refers to the prefixed user
* field being filtered, such as 'user_login', 'user_email', 'first_name', etc.
*
* @since 2.9.0
*
* @param mixed $value Value of the prefixed user field.
* @param int $user_id User ID.
*/
$value = apply_filters( "edit_user_{$field}", $value, $user_id );
}
if ( 'description' === $field ) {
$value = esc_html( $value ); // textarea_escaped?
} else {
$value = esc_attr( $value );
}
} elseif ( 'db' === $context ) {
if ( $prefixed ) {
/** This filter is documented in wp-includes/post.php */
$value = apply_filters( "pre_{$field}", $value );
} else {
/**
* Filters the value of a user field in the 'db' context.
*
* The dynamic portion of the hook name, `$field`, refers to the prefixed user
* field being filtered, such as 'user_login', 'user_email', 'first_name', etc.
*
* @since 2.9.0
*
* @param mixed $value Value of the prefixed user field.
*/
$value = apply_filters( "pre_user_{$field}", $value );
}
} else {
// Use display filters by default.
if ( $prefixed ) {
/** This filter is documented in wp-includes/post.php */
$value = apply_filters( "{$field}", $value, $user_id, $context );
} else {
/**
* Filters the value of a user field in a standard context.
*
* The dynamic portion of the hook name, `$field`, refers to the prefixed user
* field being filtered, such as 'user_login', 'user_email', 'first_name', etc.
*
* @since 2.9.0
*
* @param mixed $value The user object value to sanitize.
* @param int $user_id User ID.
* @param string $context The context to filter within.
*/
$value = apply_filters( "user_{$field}", $value, $user_id, $context );
}
}
if ( 'user_url' === $field ) {
$value = esc_url( $value );
}
if ( 'attribute' === $context ) {
$value = esc_attr( $value );
} elseif ( 'js' === $context ) {
$value = esc_js( $value );
}
// Restore the type for integer fields after esc_attr().
if ( in_array( $field, $int_fields, true ) ) {
$value = (int) $value;
}
return $value;
}
/**
* Updates all user caches.
*
* @since 3.0.0
*
* @param object|WP_User $user User object or database row to be cached
* @return void|false Void on success, false on failure.
*/
function update_user_caches( $user ) {
if ( $user instanceof WP_User ) {
if ( ! $user->exists() ) {
return false;
}
$user = $user->data;
}
wp_cache_add( $user->ID, $user, 'users' );
wp_cache_add( $user->user_login, $user->ID, 'userlogins' );
wp_cache_add( $user->user_nicename, $user->ID, 'userslugs' );
if ( ! empty( $user->user_email ) ) {
wp_cache_add( $user->user_email, $user->ID, 'useremail' );
}
}
/**
* Cleans all user caches.
*
* @since 3.0.0
* @since 4.4.0 'clean_user_cache' action was added.
* @since 6.2.0 User metadata caches are now cleared.
*
* @param WP_User|int $user User object or ID to be cleaned from the cache
*/
function clean_user_cache( $user ) {
if ( is_numeric( $user ) ) {
$user = new WP_User( $user );
}
if ( ! $user->exists() ) {
return;
}
wp_cache_delete( $user->ID, 'users' );
wp_cache_delete( $user->user_login, 'userlogins' );
wp_cache_delete( $user->user_nicename, 'userslugs' );
if ( ! empty( $user->user_email ) ) {
wp_cache_delete( $user->user_email, 'useremail' );
}
wp_cache_delete( $user->ID, 'user_meta' );
wp_cache_set_users_last_changed();
/**
* Fires immediately after the given user's cache is cleaned.
*
* @since 4.4.0
*
* @param int $user_id User ID.
* @param WP_User $user User object.
*/
do_action( 'clean_user_cache', $user->ID, $user );
}
/**
* Determines whether the given username exists.
*
* For more information on this and similar theme functions, check out
* the {@link https://developer.wordpress.org/themes/basics/conditional-tags/
* Conditional Tags} article in the Theme Developer Handbook.
*
* @since 2.0.0
*
* @param string $username The username to check for existence.
* @return int|false The user ID on success, false on failure.
*/
function username_exists( $username ) {
$user = get_user_by( 'login', $username );
if ( $user ) {
$user_id = $user->ID;
} else {
$user_id = false;
}
/**
* Filters whether the given username exists.
*
* @since 4.9.0
*
* @param int|false $user_id The user ID associated with the username,
* or false if the username does not exist.
* @param string $username The username to check for existence.
*/
return apply_filters( 'username_exists', $user_id, $username );
}
/**
* Determines whether the given email exists.
*
* For more information on this and similar theme functions, check out
* the {@link https://developer.wordpress.org/themes/basics/conditional-tags/
* Conditional Tags} article in the Theme Developer Handbook.
*
* @since 2.1.0
*
* @param string $email The email to check for existence.
* @return int|false The user ID on success, false on failure.
*/
function email_exists( $email ) {
$user = get_user_by( 'email', $email );
if ( $user ) {
$user_id = $user->ID;
} else {
$user_id = false;
}
/**
* Filters whether the given email exists.
*
* @since 5.6.0
*
* @param int|false $user_id The user ID associated with the email,
* or false if the email does not exist.
* @param string $email The email to check for existence.
*/
return apply_filters( 'email_exists', $user_id, $email );
}
/**
* Checks whether a username is valid.
*
* @since 2.0.1
* @since 4.4.0 Empty sanitized usernames are now considered invalid.
*
* @param string $username Username.
* @return bool Whether username given is valid.
*/
function validate_username( $username ) {
$sanitized = sanitize_user( $username, true );
$valid = ( $sanitized == $username && ! empty( $sanitized ) );
/**
* Filters whether the provided username is valid.
*
* @since 2.0.1
*
* @param bool $valid Whether given username is valid.
* @param string $username Username to check.
*/
return apply_filters( 'validate_username', $valid, $username );
}
/**
* Inserts a user into the database.
*
* Most of the `$userdata` array fields have filters associated with the values. Exceptions are
* 'ID', 'rich_editing', 'syntax_highlighting', 'comment_shortcuts', 'admin_color', 'use_ssl',
* 'user_registered', 'user_activation_key', 'spam', and 'role'. The filters have the prefix
* 'pre_user_' followed by the field name. An example using 'description' would have the filter
* called 'pre_user_description' that can be hooked into.
*
* @since 2.0.0
* @since 3.6.0 The `aim`, `jabber`, and `yim` fields were removed as default user contact
* methods for new installations. See wp_get_user_contact_methods().
* @since 4.7.0 The `locale` field can be passed to `$userdata`.
* @since 5.3.0 The `user_activation_key` field can be passed to `$userdata`.
* @since 5.3.0 The `spam` field can be passed to `$userdata` (Multisite only).
* @since 5.9.0 The `meta_input` field can be passed to `$userdata` to allow addition of user meta data.
*
* @global wpdb $wpdb WordPress database abstraction object.
*
* @param array|object|WP_User $userdata {
* An array, object, or WP_User object of user data arguments.
*
* @type int $ID User ID. If supplied, the user will be updated.
* @type string $user_pass The plain-text user password for new users.
* Hashed password for existing users.
* @type string $user_login The user's login username.
* @type string $user_nicename The URL-friendly user name.
* @type string $user_url The user URL.
* @type string $user_email The user email address.
* @type string $display_name The user's display name.
* Default is the user's username.
* @type string $nickname The user's nickname.
* Default is the user's username.
* @type string $first_name The user's first name. For new users, will be used
* to build the first part of the user's display name
* if `$display_name` is not specified.
* @type string $last_name The user's last name. For new users, will be used
* to build the second part of the user's display name
* if `$display_name` is not specified.
* @type string $description The user's biographical description.
* @type string $rich_editing Whether to enable the rich-editor for the user.
* Accepts 'true' or 'false' as a string literal,
* not boolean. Default 'true'.
* @type string $syntax_highlighting Whether to enable the rich code editor for the user.
* Accepts 'true' or 'false' as a string literal,
* not boolean. Default 'true'.
* @type string $comment_shortcuts Whether to enable comment moderation keyboard
* shortcuts for the user. Accepts 'true' or 'false'
* as a string literal, not boolean. Default 'false'.
* @type string $admin_color Admin color scheme for the user. Default 'fresh'.
* @type bool $use_ssl Whether the user should always access the admin over
* https. Default false.
* @type string $user_registered Date the user registered in UTC. Format is 'Y-m-d H:i:s'.
* @type string $user_activation_key Password reset key. Default empty.
* @type bool $spam Multisite only. Whether the user is marked as spam.
* Default false.
* @type string $show_admin_bar_front Whether to display the Admin Bar for the user
* on the site's front end. Accepts 'true' or 'false'
* as a string literal, not boolean. Default 'true'.
* @type string $role User's role.
* @type string $locale User's locale. Default empty.
* @type array $meta_input Array of custom user meta values keyed by meta key.
* Default empty.
* }
* @return int|WP_Error The newly created user's ID or a WP_Error object if the user could not
* be created.
*/
function wp_insert_user( $userdata ) {
global $wpdb;
if ( $userdata instanceof stdClass ) {
$userdata = get_object_vars( $userdata );
} elseif ( $userdata instanceof WP_User ) {
$userdata = $userdata->to_array();
}
// Are we updating or creating?
if ( ! empty( $userdata['ID'] ) ) {
$user_id = (int) $userdata['ID'];
$update = true;
$old_user_data = get_userdata( $user_id );
if ( ! $old_user_data ) {
return new WP_Error( 'invalid_user_id', __( 'Invalid user ID.' ) );
}
// Hashed in wp_update_user(), plaintext if called directly.
$user_pass = ! empty( $userdata['user_pass'] ) ? $userdata['user_pass'] : $old_user_data->user_pass;
} else {
$update = false;
// Hash the password.
$user_pass = wp_hash_password( $userdata['user_pass'] );
}
$sanitized_user_login = sanitize_user( $userdata['user_login'], true );
/**
* Filters a username after it has been sanitized.
*
* This filter is called before the user is created or updated.
*
* @since 2.0.3
*
* @param string $sanitized_user_login Username after it has been sanitized.
*/
$pre_user_login = apply_filters( 'pre_user_login', $sanitized_user_login );
// Remove any non-printable chars from the login string to see if we have ended up with an empty username.
$user_login = trim( $pre_user_login );
// user_login must be between 0 and 60 characters.
if ( empty( $user_login ) ) {
return new WP_Error( 'empty_user_login', __( 'Cannot create a user with an empty login name.' ) );
} elseif ( mb_strlen( $user_login ) > 60 ) {
return new WP_Error( 'user_login_too_long', __( 'Username may not be longer than 60 characters.' ) );
}
if ( ! $update && username_exists( $user_login ) ) {
return new WP_Error( 'existing_user_login', __( 'Sorry, that username already exists!' ) );
}
/**
* Filters the list of disallowed usernames.
*
* @since 4.4.0
*
* @param array $usernames Array of disallowed usernames.
*/
$illegal_logins = (array) apply_filters( 'illegal_user_logins', array() );
if ( in_array( strtolower( $user_login ), array_map( 'strtolower', $illegal_logins ), true ) ) {
return new WP_Error( 'invalid_username', __( 'Sorry, that username is not allowed.' ) );
}
/*
* If a nicename is provided, remove unsafe user characters before using it.
* Otherwise build a nicename from the user_login.
*/
if ( ! empty( $userdata['user_nicename'] ) ) {
$user_nicename = sanitize_user( $userdata['user_nicename'], true );
} else {
$user_nicename = mb_substr( $user_login, 0, 50 );
}
$user_nicename = sanitize_title( $user_nicename );
/**
* Filters a user's nicename before the user is created or updated.
*
* @since 2.0.3
*
* @param string $user_nicename The user's nicename.
*/
$user_nicename = apply_filters( 'pre_user_nicename', $user_nicename );
if ( mb_strlen( $user_nicename ) > 50 ) {
return new WP_Error( 'user_nicename_too_long', __( 'Nicename may not be longer than 50 characters.' ) );
}
$user_nicename_check = $wpdb->get_var( $wpdb->prepare( "SELECT ID FROM $wpdb->users WHERE user_nicename = %s AND user_login != %s LIMIT 1", $user_nicename, $user_login ) );
if ( $user_nicename_check ) {
$suffix = 2;
while ( $user_nicename_check ) {
// user_nicename allows 50 chars. Subtract one for a hyphen, plus the length of the suffix.
$base_length = 49 - mb_strlen( $suffix );
$alt_user_nicename = mb_substr( $user_nicename, 0, $base_length ) . "-$suffix";
$user_nicename_check = $wpdb->get_var( $wpdb->prepare( "SELECT ID FROM $wpdb->users WHERE user_nicename = %s AND user_login != %s LIMIT 1", $alt_user_nicename, $user_login ) );
++$suffix;
}
$user_nicename = $alt_user_nicename;
}
$raw_user_email = empty( $userdata['user_email'] ) ? '' : $userdata['user_email'];
/**
* Filters a user's email before the user is created or updated.
*
* @since 2.0.3
*
* @param string $raw_user_email The user's email.
*/
$user_email = apply_filters( 'pre_user_email', $raw_user_email );
/*
* If there is no update, just check for `email_exists`. If there is an update,
* check if current email and new email are the same, and check `email_exists`
* accordingly.
*/
if ( ( ! $update || ( ! empty( $old_user_data ) && 0 !== strcasecmp( $user_email, $old_user_data->user_email ) ) )
&& ! defined( 'WP_IMPORTING' )
&& email_exists( $user_email )
) {
return new WP_Error( 'existing_user_email', __( 'Sorry, that email address is already used!' ) );
}
$raw_user_url = empty( $userdata['user_url'] ) ? '' : $userdata['user_url'];
/**
* Filters a user's URL before the user is created or updated.
*
* @since 2.0.3
*
* @param string $raw_user_url The user's URL.
*/
$user_url = apply_filters( 'pre_user_url', $raw_user_url );
if ( mb_strlen( $user_url ) > 100 ) {
return new WP_Error( 'user_url_too_long', __( 'User URL may not be longer than 100 characters.' ) );
}
$user_registered = empty( $userdata['user_registered'] ) ? gmdate( 'Y-m-d H:i:s' ) : $userdata['user_registered'];
$user_activation_key = empty( $userdata['user_activation_key'] ) ? '' : $userdata['user_activation_key'];
if ( ! empty( $userdata['spam'] ) && ! is_multisite() ) {
return new WP_Error( 'no_spam', __( 'Sorry, marking a user as spam is only supported on Multisite.' ) );
}
$spam = empty( $userdata['spam'] ) ? 0 : (bool) $userdata['spam'];
// Store values to save in user meta.
$meta = array();
$nickname = empty( $userdata['nickname'] ) ? $user_login : $userdata['nickname'];
/**
* Filters a user's nickname before the user is created or updated.
*
* @since 2.0.3
*
* @param string $nickname The user's nickname.
*/
$meta['nickname'] = apply_filters( 'pre_user_nickname', $nickname );
$first_name = empty( $userdata['first_name'] ) ? '' : $userdata['first_name'];
/**
* Filters a user's first name before the user is created or updated.
*
* @since 2.0.3
*
* @param string $first_name The user's first name.
*/
$meta['first_name'] = apply_filters( 'pre_user_first_name', $first_name );
$last_name = empty( $userdata['last_name'] ) ? '' : $userdata['last_name'];
/**
* Filters a user's last name before the user is created or updated.
*
* @since 2.0.3
*
* @param string $last_name The user's last name.
*/
$meta['last_name'] = apply_filters( 'pre_user_last_name', $last_name );
if ( empty( $userdata['display_name'] ) ) {
if ( $update ) {
$display_name = $user_login;
} elseif ( $meta['first_name'] && $meta['last_name'] ) {
$display_name = sprintf(
/* translators: 1: User's first name, 2: Last name. */
_x( '%1$s %2$s', 'Display name based on first name and last name' ),
$meta['first_name'],
$meta['last_name']
);
} elseif ( $meta['first_name'] ) {
$display_name = $meta['first_name'];
} elseif ( $meta['last_name'] ) {
$display_name = $meta['last_name'];
} else {
$display_name = $user_login;
}
} else {
$display_name = $userdata['display_name'];
}
/**
* Filters a user's display name before the user is created or updated.
*
* @since 2.0.3
*
* @param string $display_name The user's display name.
*/
$display_name = apply_filters( 'pre_user_display_name', $display_name );
$description = empty( $userdata['description'] ) ? '' : $userdata['description'];
/**
* Filters a user's description before the user is created or updated.
*
* @since 2.0.3
*
* @param string $description The user's description.
*/
$meta['description'] = apply_filters( 'pre_user_description', $description );
$meta['rich_editing'] = empty( $userdata['rich_editing'] ) ? 'true' : $userdata['rich_editing'];
$meta['syntax_highlighting'] = empty( $userdata['syntax_highlighting'] ) ? 'true' : $userdata['syntax_highlighting'];
$meta['comment_shortcuts'] = empty( $userdata['comment_shortcuts'] ) || 'false' === $userdata['comment_shortcuts'] ? 'false' : 'true';
$admin_color = empty( $userdata['admin_color'] ) ? 'fresh' : $userdata['admin_color'];
$meta['admin_color'] = preg_replace( '|[^a-z0-9 _.\-@]|i', '', $admin_color );
$meta['use_ssl'] = empty( $userdata['use_ssl'] ) ? 0 : (bool) $userdata['use_ssl'];
$meta['show_admin_bar_front'] = empty( $userdata['show_admin_bar_front'] ) ? 'true' : $userdata['show_admin_bar_front'];
$meta['locale'] = isset( $userdata['locale'] ) ? $userdata['locale'] : '';
$compacted = compact( 'user_pass', 'user_nicename', 'user_email', 'user_url', 'user_registered', 'user_activation_key', 'display_name' );
$data = wp_unslash( $compacted );
if ( ! $update ) {
$data = $data + compact( 'user_login' );
}
if ( is_multisite() ) {
$data = $data + compact( 'spam' );
}
/**
* Filters user data before the record is created or updated.
*
* It only includes data in the users table, not any user metadata.
*
* @since 4.9.0
* @since 5.8.0 The `$userdata` parameter was added.
*
* @param array $data {
* Values and keys for the user.
*
* @type string $user_login The user's login. Only included if $update == false
* @type string $user_pass The user's password.
* @type string $user_email The user's email.
* @type string $user_url The user's url.
* @type string $user_nicename The user's nice name. Defaults to a URL-safe version of user's login
* @type string $display_name The user's display name.
* @type string $user_registered MySQL timestamp describing the moment when the user registered. Defaults to
* the current UTC timestamp.
* }
* @param bool $update Whether the user is being updated rather than created.
* @param int|null $user_id ID of the user to be updated, or NULL if the user is being created.
* @param array $userdata The raw array of data passed to wp_insert_user().
*/
$data = apply_filters( 'wp_pre_insert_user_data', $data, $update, ( $update ? $user_id : null ), $userdata );
if ( empty( $data ) || ! is_array( $data ) ) {
return new WP_Error( 'empty_data', __( 'Not enough data to create this user.' ) );
}
if ( $update ) {
if ( $user_email !== $old_user_data->user_email || $user_pass !== $old_user_data->user_pass ) {
$data['user_activation_key'] = '';
}
$wpdb->update( $wpdb->users, $data, array( 'ID' => $user_id ) );
} else {
$wpdb->insert( $wpdb->users, $data );
$user_id = (int) $wpdb->insert_id;
}
$user = new WP_User( $user_id );
/**
* Filters a user's meta values and keys immediately after the user is created or updated
* and before any user meta is inserted or updated.
*
* Does not include contact methods. These are added using `wp_get_user_contact_methods( $user )`.
*
* For custom meta fields, see the {@see 'insert_custom_user_meta'} filter.
*
* @since 4.4.0
* @since 5.8.0 The `$userdata` parameter was added.
*
* @param array $meta {
* Default meta values and keys for the user.
*
* @type string $nickname The user's nickname. Default is the user's username.
* @type string $first_name The user's first name.
* @type string $last_name The user's last name.
* @type string $description The user's description.
* @type string $rich_editing Whether to enable the rich-editor for the user. Default 'true'.
* @type string $syntax_highlighting Whether to enable the rich code editor for the user. Default 'true'.
* @type string $comment_shortcuts Whether to enable keyboard shortcuts for the user. Default 'false'.
* @type string $admin_color The color scheme for a user's admin screen. Default 'fresh'.
* @type int|bool $use_ssl Whether to force SSL on the user's admin area. 0|false if SSL
* is not forced.
* @type string $show_admin_bar_front Whether to show the admin bar on the front end for the user.
* Default 'true'.
* @type string $locale User's locale. Default empty.
* }
* @param WP_User $user User object.
* @param bool $update Whether the user is being updated rather than created.
* @param array $userdata The raw array of data passed to wp_insert_user().
*/
$meta = apply_filters( 'insert_user_meta', $meta, $user, $update, $userdata );
$custom_meta = array();
if ( array_key_exists( 'meta_input', $userdata ) && is_array( $userdata['meta_input'] ) && ! empty( $userdata['meta_input'] ) ) {
$custom_meta = $userdata['meta_input'];
}
/**
* Filters a user's custom meta values and keys immediately after the user is created or updated
* and before any user meta is inserted or updated.
*
* For non-custom meta fields, see the {@see 'insert_user_meta'} filter.
*
* @since 5.9.0
*
* @param array $custom_meta Array of custom user meta values keyed by meta key.
* @param WP_User $user User object.
* @param bool $update Whether the user is being updated rather than created.
* @param array $userdata The raw array of data passed to wp_insert_user().
*/
$custom_meta = apply_filters( 'insert_custom_user_meta', $custom_meta, $user, $update, $userdata );
$meta = array_merge( $meta, $custom_meta );
if ( $update ) {
// Update user meta.
foreach ( $meta as $key => $value ) {
update_user_meta( $user_id, $key, $value );
}
} else {
// Add user meta.
foreach ( $meta as $key => $value ) {
add_user_meta( $user_id, $key, $value );
}
}
foreach ( wp_get_user_contact_methods( $user ) as $key => $value ) {
if ( isset( $userdata[ $key ] ) ) {
update_user_meta( $user_id, $key, $userdata[ $key ] );
}
}
if ( isset( $userdata['role'] ) ) {
$user->set_role( $userdata['role'] );
} elseif ( ! $update ) {
$user->set_role( get_option( 'default_role' ) );
}
clean_user_cache( $user_id );
if ( $update ) {
/**
* Fires immediately after an existing user is updated.
*
* @since 2.0.0
* @since 5.8.0 The `$userdata` parameter was added.
*
* @param int $user_id User ID.
* @param WP_User $old_user_data Object containing user's data prior to update.
* @param array $userdata The raw array of data passed to wp_insert_user().
*/
do_action( 'profile_update', $user_id, $old_user_data, $userdata );
if ( isset( $userdata['spam'] ) && $userdata['spam'] != $old_user_data->spam ) {
if ( 1 == $userdata['spam'] ) {
/**
* Fires after the user is marked as a SPAM user.
*
* @since 3.0.0
*
* @param int $user_id ID of the user marked as SPAM.
*/
do_action( 'make_spam_user', $user_id );
} else {
/**
* Fires after the user is marked as a HAM user. Opposite of SPAM.
*
* @since 3.0.0
*
* @param int $user_id ID of the user marked as HAM.
*/
do_action( 'make_ham_user', $user_id );
}
}
} else {
/**
* Fires immediately after a new user is registered.
*
* @since 1.5.0
* @since 5.8.0 The `$userdata` parameter was added.
*
* @param int $user_id User ID.
* @param array $userdata The raw array of data passed to wp_insert_user().
*/
do_action( 'user_register', $user_id, $userdata );
}
return $user_id;
}
/**
* Updates a user in the database.
*
* It is possible to update a user's password by specifying the 'user_pass'
* value in the $userdata parameter array.
*
* If current user's password is being updated, then the cookies will be
* cleared.
*
* @since 2.0.0
*
* @see wp_insert_user() For what fields can be set in $userdata.
*
* @param array|object|WP_User $userdata An array of user data or a user object of type stdClass or WP_User.
* @return int|WP_Error The updated user's ID or a WP_Error object if the user could not be updated.
*/
function wp_update_user( $userdata ) {
if ( $userdata instanceof stdClass ) {
$userdata = get_object_vars( $userdata );
} elseif ( $userdata instanceof WP_User ) {
$userdata = $userdata->to_array();
}
$userdata_raw = $userdata;
$user_id = isset( $userdata['ID'] ) ? (int) $userdata['ID'] : 0;
if ( ! $user_id ) {
return new WP_Error( 'invalid_user_id', __( 'Invalid user ID.' ) );
}
// First, get all of the original fields.
$user_obj = get_userdata( $user_id );
if ( ! $user_obj ) {
return new WP_Error( 'invalid_user_id', __( 'Invalid user ID.' ) );
}
$user = $user_obj->to_array();
// Add additional custom fields.
foreach ( _get_additional_user_keys( $user_obj ) as $key ) {
$user[ $key ] = get_user_meta( $user_id, $key, true );
}
// Escape data pulled from DB.
$user = add_magic_quotes( $user );
if ( ! empty( $userdata['user_pass'] ) && $userdata['user_pass'] !== $user_obj->user_pass ) {
// If password is changing, hash it now.
$plaintext_pass = $userdata['user_pass'];
$userdata['user_pass'] = wp_hash_password( $userdata['user_pass'] );
/**
* Filters whether to send the password change email.
*
* @since 4.3.0
*
* @see wp_insert_user() For `$user` and `$userdata` fields.
*
* @param bool $send Whether to send the email.
* @param array $user The original user array.
* @param array $userdata The updated user array.
*/
$send_password_change_email = apply_filters( 'send_password_change_email', true, $user, $userdata );
}
if ( isset( $userdata['user_email'] ) && $user['user_email'] !== $userdata['user_email'] ) {
/**
* Filters whether to send the email change email.
*
* @since 4.3.0
*
* @see wp_insert_user() For `$user` and `$userdata` fields.
*
* @param bool $send Whether to send the email.
* @param array $user The original user array.
* @param array $userdata The updated user array.
*/
$send_email_change_email = apply_filters( 'send_email_change_email', true, $user, $userdata );
}
clean_user_cache( $user_obj );
// Merge old and new fields with new fields overwriting old ones.
$userdata = array_merge( $user, $userdata );
$user_id = wp_insert_user( $userdata );
if ( is_wp_error( $user_id ) ) {
return $user_id;
}
$blog_name = wp_specialchars_decode( get_option( 'blogname' ), ENT_QUOTES );
$switched_locale = false;
if ( ! empty( $send_password_change_email ) || ! empty( $send_email_change_email ) ) {
$switched_locale = switch_to_user_locale( $user_id );
}
if ( ! empty( $send_password_change_email ) ) {
/* translators: Do not translate USERNAME, ADMIN_EMAIL, EMAIL, SITENAME, SITEURL: those are placeholders. */
$pass_change_text = __(
'Hi ###USERNAME###,
This notice confirms that your password was changed on ###SITENAME###.
If you did not change your password, please contact the Site Administrator at
###ADMIN_EMAIL###
This email has been sent to ###EMAIL###
Regards,
All at ###SITENAME###
###SITEURL###'
);
$pass_change_email = array(
'to' => $user['user_email'],
/* translators: Password change notification email subject. %s: Site title. */
'subject' => __( '[%s] Password Changed' ),
'message' => $pass_change_text,
'headers' => '',
);
/**
* Filters the contents of the email sent when the user's password is changed.
*
* @since 4.3.0
*
* @param array $pass_change_email {
* Used to build wp_mail().
*
* @type string $to The intended recipients. Add emails in a comma separated string.
* @type string $subject The subject of the email.
* @type string $message The content of the email.
* The following strings have a special meaning and will get replaced dynamically:
* - ###USERNAME### The current user's username.
* - ###ADMIN_EMAIL### The admin email in case this was unexpected.
* - ###EMAIL### The user's email address.
* - ###SITENAME### The name of the site.
* - ###SITEURL### The URL to the site.
* @type string $headers Headers. Add headers in a newline (\r\n) separated string.
* }
* @param array $user The original user array.
* @param array $userdata The updated user array.
*/
$pass_change_email = apply_filters( 'password_change_email', $pass_change_email, $user, $userdata );
$pass_change_email['message'] = str_replace( '###USERNAME###', $user['user_login'], $pass_change_email['message'] );
$pass_change_email['message'] = str_replace( '###ADMIN_EMAIL###', get_option( 'admin_email' ), $pass_change_email['message'] );
$pass_change_email['message'] = str_replace( '###EMAIL###', $user['user_email'], $pass_change_email['message'] );
$pass_change_email['message'] = str_replace( '###SITENAME###', $blog_name, $pass_change_email['message'] );
$pass_change_email['message'] = str_replace( '###SITEURL###', home_url(), $pass_change_email['message'] );
wp_mail( $pass_change_email['to'], sprintf( $pass_change_email['subject'], $blog_name ), $pass_change_email['message'], $pass_change_email['headers'] );
}
if ( ! empty( $send_email_change_email ) ) {
/* translators: Do not translate USERNAME, ADMIN_EMAIL, NEW_EMAIL, EMAIL, SITENAME, SITEURL: those are placeholders. */
$email_change_text = __(
'Hi ###USERNAME###,
This notice confirms that your email address on ###SITENAME### was changed to ###NEW_EMAIL###.
If you did not change your email, please contact the Site Administrator at
###ADMIN_EMAIL###
This email has been sent to ###EMAIL###
Regards,
All at ###SITENAME###
###SITEURL###'
);
$email_change_email = array(
'to' => $user['user_email'],
/* translators: Email change notification email subject. %s: Site title. */
'subject' => __( '[%s] Email Changed' ),
'message' => $email_change_text,
'headers' => '',
);
/**
* Filters the contents of the email sent when the user's email is changed.
*
* @since 4.3.0
*
* @param array $email_change_email {
* Used to build wp_mail().
*
* @type string $to The intended recipients.
* @type string $subject The subject of the email.
* @type string $message The content of the email.
* The following strings have a special meaning and will get replaced dynamically:
* - ###USERNAME### The current user's username.
* - ###ADMIN_EMAIL### The admin email in case this was unexpected.
* - ###NEW_EMAIL### The new email address.
* - ###EMAIL### The old email address.
* - ###SITENAME### The name of the site.
* - ###SITEURL### The URL to the site.
* @type string $headers Headers.
* }
* @param array $user The original user array.
* @param array $userdata The updated user array.
*/
$email_change_email = apply_filters( 'email_change_email', $email_change_email, $user, $userdata );
$email_change_email['message'] = str_replace( '###USERNAME###', $user['user_login'], $email_change_email['message'] );
$email_change_email['message'] = str_replace( '###ADMIN_EMAIL###', get_option( 'admin_email' ), $email_change_email['message'] );
$email_change_email['message'] = str_replace( '###NEW_EMAIL###', $userdata['user_email'], $email_change_email['message'] );
$email_change_email['message'] = str_replace( '###EMAIL###', $user['user_email'], $email_change_email['message'] );
$email_change_email['message'] = str_replace( '###SITENAME###', $blog_name, $email_change_email['message'] );
$email_change_email['message'] = str_replace( '###SITEURL###', home_url(), $email_change_email['message'] );
wp_mail( $email_change_email['to'], sprintf( $email_change_email['subject'], $blog_name ), $email_change_email['message'], $email_change_email['headers'] );
}
if ( $switched_locale ) {
restore_previous_locale();
}
// Update the cookies if the password changed.
$current_user = wp_get_current_user();
if ( $current_user->ID == $user_id ) {
if ( isset( $plaintext_pass ) ) {
wp_clear_auth_cookie();
/*
* Here we calculate the expiration length of the current auth cookie and compare it to the default expiration.
* If it's greater than this, then we know the user checked 'Remember Me' when they logged in.
*/
$logged_in_cookie = wp_parse_auth_cookie( '', 'logged_in' );
/** This filter is documented in wp-includes/pluggable.php */
$default_cookie_life = apply_filters( 'auth_cookie_expiration', ( 2 * DAY_IN_SECONDS ), $user_id, false );
$remember = false;
if ( false !== $logged_in_cookie && ( $logged_in_cookie['expiration'] - time() ) > $default_cookie_life ) {
$remember = true;
}
wp_set_auth_cookie( $user_id, $remember );
}
}
/**
* Fires after the user has been updated and emails have been sent.
*
* @since 6.3.0
*
* @param int $user_id The ID of the user that was just updated.
* @param array $userdata The array of user data that was updated.
* @param array $userdata_raw The unedited array of user data that was updated.
*/
do_action( 'wp_update_user', $user_id, $userdata, $userdata_raw );
return $user_id;
}
/**
* Provides a simpler way of inserting a user into the database.
*
* Creates a new user with just the username, password, and email. For more
* complex user creation use wp_insert_user() to specify more information.
*
* @since 2.0.0
*
* @see wp_insert_user() More complete way to create a new user.
*
* @param string $username The user's username.
* @param string $password The user's password.
* @param string $email Optional. The user's email. Default empty.
* @return int|WP_Error The newly created user's ID or a WP_Error object if the user could not
* be created.
*/
function wp_create_user( $username, $password, $email = '' ) {
$user_login = wp_slash( $username );
$user_email = wp_slash( $email );
$user_pass = $password;
$userdata = compact( 'user_login', 'user_email', 'user_pass' );
return wp_insert_user( $userdata );
}
/**
* Returns a list of meta keys to be (maybe) populated in wp_update_user().
*
* The list of keys returned via this function are dependent on the presence
* of those keys in the user meta data to be set.
*
* @since 3.3.0
* @access private
*
* @param WP_User $user WP_User instance.
* @return string[] List of user keys to be populated in wp_update_user().
*/
function _get_additional_user_keys( $user ) {
$keys = array( 'first_name', 'last_name', 'nickname', 'description', 'rich_editing', 'syntax_highlighting', 'comment_shortcuts', 'admin_color', 'use_ssl', 'show_admin_bar_front', 'locale' );
return array_merge( $keys, array_keys( wp_get_user_contact_methods( $user ) ) );
}
/**
* Sets up the user contact methods.
*
* Default contact methods were removed in 3.6. A filter dictates contact methods.
*
* @since 3.7.0
*
* @param WP_User|null $user Optional. WP_User object.
* @return string[] Array of contact method labels keyed by contact method.
*/
function wp_get_user_contact_methods( $user = null ) {
$methods = array();
if ( get_site_option( 'initial_db_version' ) < 23588 ) {
$methods = array(
'aim' => __( 'AIM' ),
'yim' => __( 'Yahoo IM' ),
'jabber' => __( 'Jabber / Google Talk' ),
);
}
/**
* Filters the user contact methods.
*
* @since 2.9.0
*
* @param string[] $methods Array of contact method labels keyed by contact method.
* @param WP_User|null $user WP_User object or null if none was provided.
*/
return apply_filters( 'user_contactmethods', $methods, $user );
}
/**
* The old private function for setting up user contact methods.
*
* Use wp_get_user_contact_methods() instead.
*
* @since 2.9.0
* @access private
*
* @param WP_User|null $user Optional. WP_User object. Default null.
* @return string[] Array of contact method labels keyed by contact method.
*/
function _wp_get_user_contactmethods( $user = null ) {
return wp_get_user_contact_methods( $user );
}
/**
* Gets the text suggesting how to create strong passwords.
*
* @since 4.1.0
*
* @return string The password hint text.
*/
function wp_get_password_hint() {
$hint = __( 'Hint: The password should be at least twelve characters long. To make it stronger, use upper and lower case letters, numbers, and symbols like ! " ? $ % ^ & ).' );
/**
* Filters the text describing the site's password complexity policy.
*
* @since 4.1.0
*
* @param string $hint The password hint text.
*/
return apply_filters( 'password_hint', $hint );
}
/**
* Creates, stores, then returns a password reset key for user.
*
* @since 4.4.0
*
* @global PasswordHash $wp_hasher Portable PHP password hashing framework.
*
* @param WP_User $user User to retrieve password reset key for.
* @return string|WP_Error Password reset key on success. WP_Error on error.
*/
function get_password_reset_key( $user ) {
global $wp_hasher;
if ( ! ( $user instanceof WP_User ) ) {
return new WP_Error( 'invalidcombo', __( 'Error: There is no account with that username or email address.' ) );
}
/**
* Fires before a new password is retrieved.
*
* Use the {@see 'retrieve_password'} hook instead.
*
* @since 1.5.0
* @deprecated 1.5.1 Misspelled. Use {@see 'retrieve_password'} hook instead.
*
* @param string $user_login The user login name.
*/
do_action_deprecated( 'retreive_password', array( $user->user_login ), '1.5.1', 'retrieve_password' );
/**
* Fires before a new password is retrieved.
*
* @since 1.5.1
*
* @param string $user_login The user login name.
*/
do_action( 'retrieve_password', $user->user_login );
$password_reset_allowed = wp_is_password_reset_allowed_for_user( $user );
if ( ! $password_reset_allowed ) {
return new WP_Error( 'no_password_reset', __( 'Password reset is not allowed for this user' ) );
} elseif ( is_wp_error( $password_reset_allowed ) ) {
return $password_reset_allowed;
}
// Generate something random for a password reset key.
$key = wp_generate_password( 20, false );
/**
* Fires when a password reset key is generated.
*
* @since 2.5.0
*
* @param string $user_login The username for the user.
* @param string $key The generated password reset key.
*/
do_action( 'retrieve_password_key', $user->user_login, $key );
// Now insert the key, hashed, into the DB.
if ( empty( $wp_hasher ) ) {
require_once ABSPATH . WPINC . '/class-phpass.php';
$wp_hasher = new PasswordHash( 8, true );
}
$hashed = time() . ':' . $wp_hasher->HashPassword( $key );
$key_saved = wp_update_user(
array(
'ID' => $user->ID,
'user_activation_key' => $hashed,
)
);
if ( is_wp_error( $key_saved ) ) {
return $key_saved;
}
return $key;
}
/**
* Retrieves a user row based on password reset key and login.
*
* A key is considered 'expired' if it exactly matches the value of the
* user_activation_key field, rather than being matched after going through the
* hashing process. This field is now hashed; old values are no longer accepted
* but have a different WP_Error code so good user feedback can be provided.
*
* @since 3.1.0
*
* @global PasswordHash $wp_hasher Portable PHP password hashing framework instance.
*
* @param string $key Hash to validate sending user's password.
* @param string $login The user login.
* @return WP_User|WP_Error WP_User object on success, WP_Error object for invalid or expired keys.
*/
function check_password_reset_key( $key, $login ) {
global $wp_hasher;
$key = preg_replace( '/[^a-z0-9]/i', '', $key );
if ( empty( $key ) || ! is_string( $key ) ) {
return new WP_Error( 'invalid_key', __( 'Invalid key.' ) );
}
if ( empty( $login ) || ! is_string( $login ) ) {
return new WP_Error( 'invalid_key', __( 'Invalid key.' ) );
}
$user = get_user_by( 'login', $login );
if ( ! $user ) {
return new WP_Error( 'invalid_key', __( 'Invalid key.' ) );
}
if ( empty( $wp_hasher ) ) {
require_once ABSPATH . WPINC . '/class-phpass.php';
$wp_hasher = new PasswordHash( 8, true );
}
/**
* Filters the expiration time of password reset keys.
*
* @since 4.3.0
*
* @param int $expiration The expiration time in seconds.
*/
$expiration_duration = apply_filters( 'password_reset_expiration', DAY_IN_SECONDS );
if ( str_contains( $user->user_activation_key, ':' ) ) {
list( $pass_request_time, $pass_key ) = explode( ':', $user->user_activation_key, 2 );
$expiration_time = $pass_request_time + $expiration_duration;
} else {
$pass_key = $user->user_activation_key;
$expiration_time = false;
}
if ( ! $pass_key ) {
return new WP_Error( 'invalid_key', __( 'Invalid key.' ) );
}
$hash_is_correct = $wp_hasher->CheckPassword( $key, $pass_key );
if ( $hash_is_correct && $expiration_time && time() < $expiration_time ) {
return $user;
} elseif ( $hash_is_correct && $expiration_time ) {
// Key has an expiration time that's passed.
return new WP_Error( 'expired_key', __( 'Invalid key.' ) );
}
if ( hash_equals( $user->user_activation_key, $key ) || ( $hash_is_correct && ! $expiration_time ) ) {
$return = new WP_Error( 'expired_key', __( 'Invalid key.' ) );
$user_id = $user->ID;
/**
* Filters the return value of check_password_reset_key() when an
* old-style key is used.
*
* @since 3.7.0 Previously plain-text keys were stored in the database.
* @since 4.3.0 Previously key hashes were stored without an expiration time.
*
* @param WP_Error $return A WP_Error object denoting an expired key.
* Return a WP_User object to validate the key.
* @param int $user_id The matched user ID.
*/
return apply_filters( 'password_reset_key_expired', $return, $user_id );
}
return new WP_Error( 'invalid_key', __( 'Invalid key.' ) );
}
/**
* Handles sending a password retrieval email to a user.
*
* @since 2.5.0
* @since 5.7.0 Added `$user_login` parameter.
*
* @global wpdb $wpdb WordPress database abstraction object.
* @global PasswordHash $wp_hasher Portable PHP password hashing framework.
*
* @param string $user_login Optional. Username to send a password retrieval email for.
* Defaults to `$_POST['user_login']` if not set.
* @return true|WP_Error True when finished, WP_Error object on error.
*/
function retrieve_password( $user_login = null ) {
$errors = new WP_Error();
$user_data = false;
// Use the passed $user_login if available, otherwise use $_POST['user_login'].
if ( ! $user_login && ! empty( $_POST['user_login'] ) ) {
$user_login = $_POST['user_login'];
}
$user_login = trim( wp_unslash( $user_login ) );
if ( empty( $user_login ) ) {
$errors->add( 'empty_username', __( 'Error: Please enter a username or email address.' ) );
} elseif ( strpos( $user_login, '@' ) ) {
$user_data = get_user_by( 'email', $user_login );
if ( empty( $user_data ) ) {
$user_data = get_user_by( 'login', $user_login );
}
if ( empty( $user_data ) ) {
$errors->add( 'invalid_email', __( 'Error: There is no account with that username or email address.' ) );
}
} else {
$user_data = get_user_by( 'login', $user_login );
}
/**
* Filters the user data during a password reset request.
*
* Allows, for example, custom validation using data other than username or email address.
*
* @since 5.7.0
*
* @param WP_User|false $user_data WP_User object if found, false if the user does not exist.
* @param WP_Error $errors A WP_Error object containing any errors generated
* by using invalid credentials.
*/
$user_data = apply_filters( 'lostpassword_user_data', $user_data, $errors );
/**
* Fires before errors are returned from a password reset request.
*
* @since 2.1.0
* @since 4.4.0 Added the `$errors` parameter.
* @since 5.4.0 Added the `$user_data` parameter.
*
* @param WP_Error $errors A WP_Error object containing any errors generated
* by using invalid credentials.
* @param WP_User|false $user_data WP_User object if found, false if the user does not exist.
*/
do_action( 'lostpassword_post', $errors, $user_data );
/**
* Filters the errors encountered on a password reset request.
*
* The filtered WP_Error object may, for example, contain errors for an invalid
* username or email address. A WP_Error object should always be returned,
* but may or may not contain errors.
*
* If any errors are present in $errors, this will abort the password reset request.
*
* @since 5.5.0
*
* @param WP_Error $errors A WP_Error object containing any errors generated
* by using invalid credentials.
* @param WP_User|false $user_data WP_User object if found, false if the user does not exist.
*/
$errors = apply_filters( 'lostpassword_errors', $errors, $user_data );
if ( $errors->has_errors() ) {
return $errors;
}
if ( ! $user_data ) {
$errors->add( 'invalidcombo', __( 'Error: There is no account with that username or email address.' ) );
return $errors;
}
/**
* Filters whether to send the retrieve password email.
*
* Return false to disable sending the email.
*
* @since 6.0.0
*
* @param bool $send Whether to send the email.
* @param string $user_login The username for the user.
* @param WP_User $user_data WP_User object.
*/
if ( ! apply_filters( 'send_retrieve_password_email', true, $user_login, $user_data ) ) {
return true;
}
// Redefining user_login ensures we return the right case in the email.
$user_login = $user_data->user_login;
$user_email = $user_data->user_email;
$key = get_password_reset_key( $user_data );
if ( is_wp_error( $key ) ) {
return $key;
}
// Localize password reset message content for user.
$locale = get_user_locale( $user_data );
$switched_locale = switch_to_user_locale( $user_data->ID );
if ( is_multisite() ) {
$site_name = get_network()->site_name;
} else {
/*
* The blogname option is escaped with esc_html on the way into the database
* in sanitize_option. We want to reverse this for the plain text arena of emails.
*/
$site_name = wp_specialchars_decode( get_option( 'blogname' ), ENT_QUOTES );
}
$message = __( 'Someone has requested a password reset for the following account:' ) . "\r\n\r\n";
/* translators: %s: Site name. */
$message .= sprintf( __( 'Site Name: %s' ), $site_name ) . "\r\n\r\n";
/* translators: %s: User login. */
$message .= sprintf( __( 'Username: %s' ), $user_login ) . "\r\n\r\n";
$message .= __( 'If this was a mistake, ignore this email and nothing will happen.' ) . "\r\n\r\n";
$message .= __( 'To reset your password, visit the following address:' ) . "\r\n\r\n";
$message .= network_site_url( "wp-login.php?action=rp&key=$key&login=" . rawurlencode( $user_login ), 'login' ) . '&wp_lang=' . $locale . "\r\n\r\n";
if ( ! is_user_logged_in() ) {
$requester_ip = $_SERVER['REMOTE_ADDR'];
if ( $requester_ip ) {
$message .= sprintf(
/* translators: %s: IP address of password reset requester. */
__( 'This password reset request originated from the IP address %s.' ),
$requester_ip
) . "\r\n";
}
}
/* translators: Password reset notification email subject. %s: Site title. */
$title = sprintf( __( '[%s] Password Reset' ), $site_name );
/**
* Filters the subject of the password reset email.
*
* @since 2.8.0
* @since 4.4.0 Added the `$user_login` and `$user_data` parameters.
*
* @param string $title Email subject.
* @param string $user_login The username for the user.
* @param WP_User $user_data WP_User object.
*/
$title = apply_filters( 'retrieve_password_title', $title, $user_login, $user_data );
/**
* Filters the message body of the password reset mail.
*
* If the filtered message is empty, the password reset email will not be sent.
*
* @since 2.8.0
* @since 4.1.0 Added `$user_login` and `$user_data` parameters.
*
* @param string $message Email message.
* @param string $key The activation key.
* @param string $user_login The username for the user.
* @param WP_User $user_data WP_User object.
*/
$message = apply_filters( 'retrieve_password_message', $message, $key, $user_login, $user_data );
// Short-circuit on falsey $message value for backwards compatibility.
if ( ! $message ) {
return true;
}
/*
* Wrap the single notification email arguments in an array
* to pass them to the retrieve_password_notification_email filter.
*/
$defaults = array(
'to' => $user_email,
'subject' => $title,
'message' => $message,
'headers' => '',
);
/**
* Filters the contents of the reset password notification email sent to the user.
*
* @since 6.0.0
*
* @param array $defaults {
* The default notification email arguments. Used to build wp_mail().
*
* @type string $to The intended recipient - user email address.
* @type string $subject The subject of the email.
* @type string $message The body of the email.
* @type string $headers The headers of the email.
* }
* @type string $key The activation key.
* @type string $user_login The username for the user.
* @type WP_User $user_data WP_User object.
*/
$notification_email = apply_filters( 'retrieve_password_notification_email', $defaults, $key, $user_login, $user_data );
if ( $switched_locale ) {
restore_previous_locale();
}
if ( is_array( $notification_email ) ) {
// Force key order and merge defaults in case any value is missing in the filtered array.
$notification_email = array_merge( $defaults, $notification_email );
} else {
$notification_email = $defaults;
}
list( $to, $subject, $message, $headers ) = array_values( $notification_email );
$subject = wp_specialchars_decode( $subject );
if ( ! wp_mail( $to, $subject, $message, $headers ) ) {
$errors->add(
'retrieve_password_email_failure',
sprintf(
/* translators: %s: Documentation URL. */
__( 'Error: The email could not be sent. Your site may not be correctly configured to send emails. Get support for resetting your password.' ),
esc_url( __( 'https://wordpress.org/documentation/article/reset-your-password/' ) )
)
);
return $errors;
}
return true;
}
/**
* Handles resetting the user's password.
*
* @since 2.5.0
*
* @param WP_User $user The user
* @param string $new_pass New password for the user in plaintext
*/
function reset_password( $user, $new_pass ) {
/**
* Fires before the user's password is reset.
*
* @since 1.5.0
*
* @param WP_User $user The user.
* @param string $new_pass New user password.
*/
do_action( 'password_reset', $user, $new_pass );
wp_set_password( $new_pass, $user->ID );
update_user_meta( $user->ID, 'default_password_nag', false );
/**
* Fires after the user's password is reset.
*
* @since 4.4.0
*
* @param WP_User $user The user.
* @param string $new_pass New user password.
*/
do_action( 'after_password_reset', $user, $new_pass );
}
/**
* Handles registering a new user.
*
* @since 2.5.0
*
* @param string $user_login User's username for logging in
* @param string $user_email User's email address to send password and add
* @return int|WP_Error Either user's ID or error on failure.
*/
function register_new_user( $user_login, $user_email ) {
$errors = new WP_Error();
$sanitized_user_login = sanitize_user( $user_login );
/**
* Filters the email address of a user being registered.
*
* @since 2.1.0
*
* @param string $user_email The email address of the new user.
*/
$user_email = apply_filters( 'user_registration_email', $user_email );
// Check the username.
if ( '' === $sanitized_user_login ) {
$errors->add( 'empty_username', __( 'Error: Please enter a username.' ) );
} elseif ( ! validate_username( $user_login ) ) {
$errors->add( 'invalid_username', __( 'Error: This username is invalid because it uses illegal characters. Please enter a valid username.' ) );
$sanitized_user_login = '';
} elseif ( username_exists( $sanitized_user_login ) ) {
$errors->add( 'username_exists', __( 'Error: This username is already registered. Please choose another one.' ) );
} else {
/** This filter is documented in wp-includes/user.php */
$illegal_user_logins = (array) apply_filters( 'illegal_user_logins', array() );
if ( in_array( strtolower( $sanitized_user_login ), array_map( 'strtolower', $illegal_user_logins ), true ) ) {
$errors->add( 'invalid_username', __( 'Error: Sorry, that username is not allowed.' ) );
}
}
// Check the email address.
if ( '' === $user_email ) {
$errors->add( 'empty_email', __( 'Error: Please type your email address.' ) );
} elseif ( ! is_email( $user_email ) ) {
$errors->add( 'invalid_email', __( 'Error: The email address is not correct.' ) );
$user_email = '';
} elseif ( email_exists( $user_email ) ) {
$errors->add(
'email_exists',
sprintf(
/* translators: %s: Link to the login page. */
__( 'Error: This email address is already registered. Log in with this address or choose another one.' ),
wp_login_url()
)
);
}
/**
* Fires when submitting registration form data, before the user is created.
*
* @since 2.1.0
*
* @param string $sanitized_user_login The submitted username after being sanitized.
* @param string $user_email The submitted email.
* @param WP_Error $errors Contains any errors with submitted username and email,
* e.g., an empty field, an invalid username or email,
* or an existing username or email.
*/
do_action( 'register_post', $sanitized_user_login, $user_email, $errors );
/**
* Filters the errors encountered when a new user is being registered.
*
* The filtered WP_Error object may, for example, contain errors for an invalid
* or existing username or email address. A WP_Error object should always be returned,
* but may or may not contain errors.
*
* If any errors are present in $errors, this will abort the user's registration.
*
* @since 2.1.0
*
* @param WP_Error $errors A WP_Error object containing any errors encountered
* during registration.
* @param string $sanitized_user_login User's username after it has been sanitized.
* @param string $user_email User's email.
*/
$errors = apply_filters( 'registration_errors', $errors, $sanitized_user_login, $user_email );
if ( $errors->has_errors() ) {
return $errors;
}
$user_pass = wp_generate_password( 12, false );
$user_id = wp_create_user( $sanitized_user_login, $user_pass, $user_email );
if ( ! $user_id || is_wp_error( $user_id ) ) {
$errors->add(
'registerfail',
sprintf(
/* translators: %s: Admin email address. */
__( 'Error: Could not register you… please contact the site admin!' ),
get_option( 'admin_email' )
)
);
return $errors;
}
update_user_meta( $user_id, 'default_password_nag', true ); // Set up the password change nag.
if ( ! empty( $_COOKIE['wp_lang'] ) ) {
$wp_lang = sanitize_text_field( $_COOKIE['wp_lang'] );
if ( in_array( $wp_lang, get_available_languages(), true ) ) {
update_user_meta( $user_id, 'locale', $wp_lang ); // Set user locale if defined on registration.
}
}
/**
* Fires after a new user registration has been recorded.
*
* @since 4.4.0
*
* @param int $user_id ID of the newly registered user.
*/
do_action( 'register_new_user', $user_id );
return $user_id;
}
/**
* Initiates email notifications related to the creation of new users.
*
* Notifications are sent both to the site admin and to the newly created user.
*
* @since 4.4.0
* @since 4.6.0 Converted the `$notify` parameter to accept 'user' for sending
* notifications only to the user created.
*
* @param int $user_id ID of the newly created user.
* @param string $notify Optional. Type of notification that should happen. Accepts 'admin'
* or an empty string (admin only), 'user', or 'both' (admin and user).
* Default 'both'.
*/
function wp_send_new_user_notifications( $user_id, $notify = 'both' ) {
wp_new_user_notification( $user_id, null, $notify );
}
/**
* Retrieves the current session token from the logged_in cookie.
*
* @since 4.0.0
*
* @return string Token.
*/
function wp_get_session_token() {
$cookie = wp_parse_auth_cookie( '', 'logged_in' );
return ! empty( $cookie['token'] ) ? $cookie['token'] : '';
}
/**
* Retrieves a list of sessions for the current user.
*
* @since 4.0.0
*
* @return array Array of sessions.
*/
function wp_get_all_sessions() {
$manager = WP_Session_Tokens::get_instance( get_current_user_id() );
return $manager->get_all();
}
/**
* Removes the current session token from the database.
*
* @since 4.0.0
*/
function wp_destroy_current_session() {
$token = wp_get_session_token();
if ( $token ) {
$manager = WP_Session_Tokens::get_instance( get_current_user_id() );
$manager->destroy( $token );
}
}
/**
* Removes all but the current session token for the current user for the database.
*
* @since 4.0.0
*/
function wp_destroy_other_sessions() {
$token = wp_get_session_token();
if ( $token ) {
$manager = WP_Session_Tokens::get_instance( get_current_user_id() );
$manager->destroy_others( $token );
}
}
/**
* Removes all session tokens for the current user from the database.
*
* @since 4.0.0
*/
function wp_destroy_all_sessions() {
$manager = WP_Session_Tokens::get_instance( get_current_user_id() );
$manager->destroy_all();
}
/**
* Gets the user IDs of all users with no role on this site.
*
* @since 4.4.0
* @since 4.9.0 The `$site_id` parameter was added to support multisite.
*
* @global wpdb $wpdb WordPress database abstraction object.
*
* @param int|null $site_id Optional. The site ID to get users with no role for. Defaults to the current site.
* @return string[] Array of user IDs as strings.
*/
function wp_get_users_with_no_role( $site_id = null ) {
global $wpdb;
if ( ! $site_id ) {
$site_id = get_current_blog_id();
}
$prefix = $wpdb->get_blog_prefix( $site_id );
if ( is_multisite() && get_current_blog_id() != $site_id ) {
switch_to_blog( $site_id );
$role_names = wp_roles()->get_names();
restore_current_blog();
} else {
$role_names = wp_roles()->get_names();
}
$regex = implode( '|', array_keys( $role_names ) );
$regex = preg_replace( '/[^a-zA-Z_\|-]/', '', $regex );
$users = $wpdb->get_col(
$wpdb->prepare(
"SELECT user_id
FROM $wpdb->usermeta
WHERE meta_key = '{$prefix}capabilities'
AND meta_value NOT REGEXP %s",
$regex
)
);
return $users;
}
/**
* Retrieves the current user object.
*
* Will set the current user, if the current user is not set. The current user
* will be set to the logged-in person. If no user is logged-in, then it will
* set the current user to 0, which is invalid and won't have any permissions.
*
* This function is used by the pluggable functions wp_get_current_user() and
* get_currentuserinfo(), the latter of which is deprecated but used for backward
* compatibility.
*
* @since 4.5.0
* @access private
*
* @see wp_get_current_user()
* @global WP_User $current_user Checks if the current user is set.
*
* @return WP_User Current WP_User instance.
*/
function _wp_get_current_user() {
global $current_user;
if ( ! empty( $current_user ) ) {
if ( $current_user instanceof WP_User ) {
return $current_user;
}
// Upgrade stdClass to WP_User.
if ( is_object( $current_user ) && isset( $current_user->ID ) ) {
$cur_id = $current_user->ID;
$current_user = null;
wp_set_current_user( $cur_id );
return $current_user;
}
// $current_user has a junk value. Force to WP_User with ID 0.
$current_user = null;
wp_set_current_user( 0 );
return $current_user;
}
if ( defined( 'XMLRPC_REQUEST' ) && XMLRPC_REQUEST ) {
wp_set_current_user( 0 );
return $current_user;
}
/**
* Filters the current user.
*
* The default filters use this to determine the current user from the
* request's cookies, if available.
*
* Returning a value of false will effectively short-circuit setting
* the current user.
*
* @since 3.9.0
*
* @param int|false $user_id User ID if one has been determined, false otherwise.
*/
$user_id = apply_filters( 'determine_current_user', false );
if ( ! $user_id ) {
wp_set_current_user( 0 );
return $current_user;
}
wp_set_current_user( $user_id );
return $current_user;
}
/**
* Sends a confirmation request email when a change of user email address is attempted.
*
* @since 3.0.0
* @since 4.9.0 This function was moved from wp-admin/includes/ms.php so it's no longer Multisite specific.
*
* @global WP_Error $errors WP_Error object.
*/
function send_confirmation_on_profile_email() {
global $errors;
$current_user = wp_get_current_user();
if ( ! is_object( $errors ) ) {
$errors = new WP_Error();
}
if ( $current_user->ID != $_POST['user_id'] ) {
return false;
}
if ( $current_user->user_email != $_POST['email'] ) {
if ( ! is_email( $_POST['email'] ) ) {
$errors->add(
'user_email',
__( 'Error: The email address is not correct.' ),
array(
'form-field' => 'email',
)
);
return;
}
if ( email_exists( $_POST['email'] ) ) {
$errors->add(
'user_email',
__( 'Error: The email address is already used.' ),
array(
'form-field' => 'email',
)
);
delete_user_meta( $current_user->ID, '_new_email' );
return;
}
$hash = md5( $_POST['email'] . time() . wp_rand() );
$new_user_email = array(
'hash' => $hash,
'newemail' => $_POST['email'],
);
update_user_meta( $current_user->ID, '_new_email', $new_user_email );
$sitename = wp_specialchars_decode( get_option( 'blogname' ), ENT_QUOTES );
/* translators: Do not translate USERNAME, ADMIN_URL, EMAIL, SITENAME, SITEURL: those are placeholders. */
$email_text = __(
'Howdy ###USERNAME###,
You recently requested to have the email address on your account changed.
If this is correct, please click on the following link to change it:
###ADMIN_URL###
You can safely ignore and delete this email if you do not want to
take this action.
This email has been sent to ###EMAIL###
Regards,
All at ###SITENAME###
###SITEURL###'
);
/**
* Filters the text of the email sent when a change of user email address is attempted.
*
* The following strings have a special meaning and will get replaced dynamically:
* - ###USERNAME### The current user's username.
* - ###ADMIN_URL### The link to click on to confirm the email change.
* - ###EMAIL### The new email.
* - ###SITENAME### The name of the site.
* - ###SITEURL### The URL to the site.
*
* @since MU (3.0.0)
* @since 4.9.0 This filter is no longer Multisite specific.
*
* @param string $email_text Text in the email.
* @param array $new_user_email {
* Data relating to the new user email address.
*
* @type string $hash The secure hash used in the confirmation link URL.
* @type string $newemail The proposed new email address.
* }
*/
$content = apply_filters( 'new_user_email_content', $email_text, $new_user_email );
$content = str_replace( '###USERNAME###', $current_user->user_login, $content );
$content = str_replace( '###ADMIN_URL###', esc_url( self_admin_url( 'profile.php?newuseremail=' . $hash ) ), $content );
$content = str_replace( '###EMAIL###', $_POST['email'], $content );
$content = str_replace( '###SITENAME###', $sitename, $content );
$content = str_replace( '###SITEURL###', home_url(), $content );
/* translators: New email address notification email subject. %s: Site title. */
wp_mail( $_POST['email'], sprintf( __( '[%s] Email Change Request' ), $sitename ), $content );
$_POST['email'] = $current_user->user_email;
}
}
/**
* Adds an admin notice alerting the user to check for confirmation request email
* after email address change.
*
* @since 3.0.0
* @since 4.9.0 This function was moved from wp-admin/includes/ms.php so it's no longer Multisite specific.
*
* @global string $pagenow The filename of the current screen.
*/
function new_user_email_admin_notice() {
global $pagenow;
if ( 'profile.php' === $pagenow && isset( $_GET['updated'] ) ) {
$email = get_user_meta( get_current_user_id(), '_new_email', true );
if ( $email ) {
$message = sprintf(
/* translators: %s: New email address. */
__( 'Your email address has not been updated yet. Please check your inbox at %s for a confirmation email.' ),
'' . esc_html( $email['newemail'] ) . '
'
);
wp_admin_notice( $message, array( 'type' => 'info' ) );
}
}
}
/**
* Gets all personal data request types.
*
* @since 4.9.6
* @access private
*
* @return string[] List of core privacy action types.
*/
function _wp_privacy_action_request_types() {
return array(
'export_personal_data',
'remove_personal_data',
);
}
/**
* Registers the personal data exporter for users.
*
* @since 4.9.6
*
* @param array[] $exporters An array of personal data exporters.
* @return array[] An array of personal data exporters.
*/
function wp_register_user_personal_data_exporter( $exporters ) {
$exporters['wordpress-user'] = array(
'exporter_friendly_name' => __( 'WordPress User' ),
'callback' => 'wp_user_personal_data_exporter',
);
return $exporters;
}
/**
* Finds and exports personal data associated with an email address from the user and user_meta table.
*
* @since 4.9.6
* @since 5.4.0 Added 'Community Events Location' group to the export data.
* @since 5.4.0 Added 'Session Tokens' group to the export data.
*
* @param string $email_address The user's email address.
* @return array {
* An array of personal data.
*
* @type array[] $data An array of personal data arrays.
* @type bool $done Whether the exporter is finished.
* }
*/
function wp_user_personal_data_exporter( $email_address ) {
$email_address = trim( $email_address );
$data_to_export = array();
$user = get_user_by( 'email', $email_address );
if ( ! $user ) {
return array(
'data' => array(),
'done' => true,
);
}
$user_meta = get_user_meta( $user->ID );
$user_props_to_export = array(
'ID' => __( 'User ID' ),
'user_login' => __( 'User Login Name' ),
'user_nicename' => __( 'User Nice Name' ),
'user_email' => __( 'User Email' ),
'user_url' => __( 'User URL' ),
'user_registered' => __( 'User Registration Date' ),
'display_name' => __( 'User Display Name' ),
'nickname' => __( 'User Nickname' ),
'first_name' => __( 'User First Name' ),
'last_name' => __( 'User Last Name' ),
'description' => __( 'User Description' ),
);
$user_data_to_export = array();
foreach ( $user_props_to_export as $key => $name ) {
$value = '';
switch ( $key ) {
case 'ID':
case 'user_login':
case 'user_nicename':
case 'user_email':
case 'user_url':
case 'user_registered':
case 'display_name':
$value = $user->data->$key;
break;
case 'nickname':
case 'first_name':
case 'last_name':
case 'description':
$value = $user_meta[ $key ][0];
break;
}
if ( ! empty( $value ) ) {
$user_data_to_export[] = array(
'name' => $name,
'value' => $value,
);
}
}
// Get the list of reserved names.
$reserved_names = array_values( $user_props_to_export );
/**
* Filters the user's profile data for the privacy exporter.
*
* @since 5.4.0
*
* @param array $additional_user_profile_data {
* An array of name-value pairs of additional user data items. Default empty array.
*
* @type string $name The user-facing name of an item name-value pair,e.g. 'IP Address'.
* @type string $value The user-facing value of an item data pair, e.g. '50.60.70.0'.
* }
* @param WP_User $user The user whose data is being exported.
* @param string[] $reserved_names An array of reserved names. Any item in `$additional_user_data`
* that uses one of these for its `name` will not be included in the export.
*/
$_extra_data = apply_filters( 'wp_privacy_additional_user_profile_data', array(), $user, $reserved_names );
if ( is_array( $_extra_data ) && ! empty( $_extra_data ) ) {
// Remove items that use reserved names.
$extra_data = array_filter(
$_extra_data,
static function ( $item ) use ( $reserved_names ) {
return ! in_array( $item['name'], $reserved_names, true );
}
);
if ( count( $extra_data ) !== count( $_extra_data ) ) {
_doing_it_wrong(
__FUNCTION__,
sprintf(
/* translators: %s: wp_privacy_additional_user_profile_data */
__( 'Filter %s returned items with reserved names.' ),
'wp_privacy_additional_user_profile_data
'
),
'5.4.0'
);
}
if ( ! empty( $extra_data ) ) {
$user_data_to_export = array_merge( $user_data_to_export, $extra_data );
}
}
$data_to_export[] = array(
'group_id' => 'user',
'group_label' => __( 'User' ),
'group_description' => __( 'User’s profile data.' ),
'item_id' => "user-{$user->ID}",
'data' => $user_data_to_export,
);
if ( isset( $user_meta['community-events-location'] ) ) {
$location = maybe_unserialize( $user_meta['community-events-location'][0] );
$location_props_to_export = array(
'description' => __( 'City' ),
'country' => __( 'Country' ),
'latitude' => __( 'Latitude' ),
'longitude' => __( 'Longitude' ),
'ip' => __( 'IP' ),
);
$location_data_to_export = array();
foreach ( $location_props_to_export as $key => $name ) {
if ( ! empty( $location[ $key ] ) ) {
$location_data_to_export[] = array(
'name' => $name,
'value' => $location[ $key ],
);
}
}
$data_to_export[] = array(
'group_id' => 'community-events-location',
'group_label' => __( 'Community Events Location' ),
'group_description' => __( 'User’s location data used for the Community Events in the WordPress Events and News dashboard widget.' ),
'item_id' => "community-events-location-{$user->ID}",
'data' => $location_data_to_export,
);
}
if ( isset( $user_meta['session_tokens'] ) ) {
$session_tokens = maybe_unserialize( $user_meta['session_tokens'][0] );
$session_tokens_props_to_export = array(
'expiration' => __( 'Expiration' ),
'ip' => __( 'IP' ),
'ua' => __( 'User Agent' ),
'login' => __( 'Last Login' ),
);
foreach ( $session_tokens as $token_key => $session_token ) {
$session_tokens_data_to_export = array();
foreach ( $session_tokens_props_to_export as $key => $name ) {
if ( ! empty( $session_token[ $key ] ) ) {
$value = $session_token[ $key ];
if ( in_array( $key, array( 'expiration', 'login' ), true ) ) {
$value = date_i18n( 'F d, Y H:i A', $value );
}
$session_tokens_data_to_export[] = array(
'name' => $name,
'value' => $value,
);
}
}
$data_to_export[] = array(
'group_id' => 'session-tokens',
'group_label' => __( 'Session Tokens' ),
'group_description' => __( 'User’s Session Tokens data.' ),
'item_id' => "session-tokens-{$user->ID}-{$token_key}",
'data' => $session_tokens_data_to_export,
);
}
}
return array(
'data' => $data_to_export,
'done' => true,
);
}
/**
* Updates log when privacy request is confirmed.
*
* @since 4.9.6
* @access private
*
* @param int $request_id ID of the request.
*/
function _wp_privacy_account_request_confirmed( $request_id ) {
$request = wp_get_user_request( $request_id );
if ( ! $request ) {
return;
}
if ( ! in_array( $request->status, array( 'request-pending', 'request-failed' ), true ) ) {
return;
}
update_post_meta( $request_id, '_wp_user_request_confirmed_timestamp', time() );
wp_update_post(
array(
'ID' => $request_id,
'post_status' => 'request-confirmed',
)
);
}
/**
* Notifies the site administrator via email when a request is confirmed.
*
* Without this, the admin would have to manually check the site to see if any
* action was needed on their part yet.
*
* @since 4.9.6
*
* @param int $request_id The ID of the request.
*/
function _wp_privacy_send_request_confirmation_notification( $request_id ) {
$request = wp_get_user_request( $request_id );
if ( ! ( $request instanceof WP_User_Request ) || 'request-confirmed' !== $request->status ) {
return;
}
$already_notified = (bool) get_post_meta( $request_id, '_wp_admin_notified', true );
if ( $already_notified ) {
return;
}
if ( 'export_personal_data' === $request->action_name ) {
$manage_url = admin_url( 'export-personal-data.php' );
} elseif ( 'remove_personal_data' === $request->action_name ) {
$manage_url = admin_url( 'erase-personal-data.php' );
}
$action_description = wp_user_request_action_description( $request->action_name );
/**
* Filters the recipient of the data request confirmation notification.
*
* In a Multisite environment, this will default to the email address of the
* network admin because, by default, single site admins do not have the
* capabilities required to process requests. Some networks may wish to
* delegate those capabilities to a single-site admin, or a dedicated person
* responsible for managing privacy requests.
*
* @since 4.9.6
*
* @param string $admin_email The email address of the notification recipient.
* @param WP_User_Request $request The request that is initiating the notification.
*/
$admin_email = apply_filters( 'user_request_confirmed_email_to', get_site_option( 'admin_email' ), $request );
$email_data = array(
'request' => $request,
'user_email' => $request->email,
'description' => $action_description,
'manage_url' => $manage_url,
'sitename' => wp_specialchars_decode( get_option( 'blogname' ), ENT_QUOTES ),
'siteurl' => home_url(),
'admin_email' => $admin_email,
);
$subject = sprintf(
/* translators: Privacy data request confirmed notification email subject. 1: Site title, 2: Name of the confirmed action. */
__( '[%1$s] Action Confirmed: %2$s' ),
$email_data['sitename'],
$action_description
);
/**
* Filters the subject of the user request confirmation email.
*
* @since 4.9.8
*
* @param string $subject The email subject.
* @param string $sitename The name of the site.
* @param array $email_data {
* Data relating to the account action email.
*
* @type WP_User_Request $request User request object.
* @type string $user_email The email address confirming a request
* @type string $description Description of the action being performed so the user knows what the email is for.
* @type string $manage_url The link to click manage privacy requests of this type.
* @type string $sitename The site name sending the mail.
* @type string $siteurl The site URL sending the mail.
* @type string $admin_email The administrator email receiving the mail.
* }
*/
$subject = apply_filters( 'user_request_confirmed_email_subject', $subject, $email_data['sitename'], $email_data );
/* translators: Do not translate SITENAME, USER_EMAIL, DESCRIPTION, MANAGE_URL, SITEURL; those are placeholders. */
$content = __(
'Howdy,
A user data privacy request has been confirmed on ###SITENAME###:
User: ###USER_EMAIL###
Request: ###DESCRIPTION###
You can view and manage these data privacy requests here:
###MANAGE_URL###
Regards,
All at ###SITENAME###
###SITEURL###'
);
/**
* Filters the body of the user request confirmation email.
*
* The email is sent to an administrator when a user request is confirmed.
*
* The following strings have a special meaning and will get replaced dynamically:
*
* ###SITENAME### The name of the site.
* ###USER_EMAIL### The user email for the request.
* ###DESCRIPTION### Description of the action being performed so the user knows what the email is for.
* ###MANAGE_URL### The URL to manage requests.
* ###SITEURL### The URL to the site.
*
* @since 4.9.6
* @deprecated 5.8.0 Use {@see 'user_request_confirmed_email_content'} instead.
* For user erasure fulfillment email content
* use {@see 'user_erasure_fulfillment_email_content'} instead.
*
* @param string $content The email content.
* @param array $email_data {
* Data relating to the account action email.
*
* @type WP_User_Request $request User request object.
* @type string $user_email The email address confirming a request
* @type string $description Description of the action being performed
* so the user knows what the email is for.
* @type string $manage_url The link to click manage privacy requests of this type.
* @type string $sitename The site name sending the mail.
* @type string $siteurl The site URL sending the mail.
* @type string $admin_email The administrator email receiving the mail.
* }
*/
$content = apply_filters_deprecated(
'user_confirmed_action_email_content',
array( $content, $email_data ),
'5.8.0',
sprintf(
/* translators: 1 & 2: Deprecation replacement options. */
__( '%1$s or %2$s' ),
'user_request_confirmed_email_content',
'user_erasure_fulfillment_email_content'
)
);
/**
* Filters the body of the user request confirmation email.
*
* The email is sent to an administrator when a user request is confirmed.
* The following strings have a special meaning and will get replaced dynamically:
*
* ###SITENAME### The name of the site.
* ###USER_EMAIL### The user email for the request.
* ###DESCRIPTION### Description of the action being performed so the user knows what the email is for.
* ###MANAGE_URL### The URL to manage requests.
* ###SITEURL### The URL to the site.
*
* @since 5.8.0
*
* @param string $content The email content.
* @param array $email_data {
* Data relating to the account action email.
*
* @type WP_User_Request $request User request object.
* @type string $user_email The email address confirming a request
* @type string $description Description of the action being performed so the user knows what the email is for.
* @type string $manage_url The link to click manage privacy requests of this type.
* @type string $sitename The site name sending the mail.
* @type string $siteurl The site URL sending the mail.
* @type string $admin_email The administrator email receiving the mail.
* }
*/
$content = apply_filters( 'user_request_confirmed_email_content', $content, $email_data );
$content = str_replace( '###SITENAME###', $email_data['sitename'], $content );
$content = str_replace( '###USER_EMAIL###', $email_data['user_email'], $content );
$content = str_replace( '###DESCRIPTION###', $email_data['description'], $content );
$content = str_replace( '###MANAGE_URL###', sanitize_url( $email_data['manage_url'] ), $content );
$content = str_replace( '###SITEURL###', sanitize_url( $email_data['siteurl'] ), $content );
$headers = '';
/**
* Filters the headers of the user request confirmation email.
*
* @since 5.4.0
*
* @param string|array $headers The email headers.
* @param string $subject The email subject.
* @param string $content The email content.
* @param int $request_id The request ID.
* @param array $email_data {
* Data relating to the account action email.
*
* @type WP_User_Request $request User request object.
* @type string $user_email The email address confirming a request
* @type string $description Description of the action being performed so the user knows what the email is for.
* @type string $manage_url The link to click manage privacy requests of this type.
* @type string $sitename The site name sending the mail.
* @type string $siteurl The site URL sending the mail.
* @type string $admin_email The administrator email receiving the mail.
* }
*/
$headers = apply_filters( 'user_request_confirmed_email_headers', $headers, $subject, $content, $request_id, $email_data );
$email_sent = wp_mail( $email_data['admin_email'], $subject, $content, $headers );
if ( $email_sent ) {
update_post_meta( $request_id, '_wp_admin_notified', true );
}
}
/**
* Notifies the user when their erasure request is fulfilled.
*
* Without this, the user would never know if their data was actually erased.
*
* @since 4.9.6
*
* @param int $request_id The privacy request post ID associated with this request.
*/
function _wp_privacy_send_erasure_fulfillment_notification( $request_id ) {
$request = wp_get_user_request( $request_id );
if ( ! ( $request instanceof WP_User_Request ) || 'request-completed' !== $request->status ) {
return;
}
$already_notified = (bool) get_post_meta( $request_id, '_wp_user_notified', true );
if ( $already_notified ) {
return;
}
// Localize message content for user; fallback to site default for visitors.
if ( ! empty( $request->user_id ) ) {
$switched_locale = switch_to_user_locale( $request->user_id );
} else {
$switched_locale = switch_to_locale( get_locale() );
}
/**
* Filters the recipient of the data erasure fulfillment notification.
*
* @since 4.9.6
*
* @param string $user_email The email address of the notification recipient.
* @param WP_User_Request $request The request that is initiating the notification.
*/
$user_email = apply_filters( 'user_erasure_fulfillment_email_to', $request->email, $request );
$email_data = array(
'request' => $request,
'message_recipient' => $user_email,
'privacy_policy_url' => get_privacy_policy_url(),
'sitename' => wp_specialchars_decode( get_option( 'blogname' ), ENT_QUOTES ),
'siteurl' => home_url(),
);
$subject = sprintf(
/* translators: Erasure request fulfilled notification email subject. %s: Site title. */
__( '[%s] Erasure Request Fulfilled' ),
$email_data['sitename']
);
/**
* Filters the subject of the email sent when an erasure request is completed.
*
* @since 4.9.8
* @deprecated 5.8.0 Use {@see 'user_erasure_fulfillment_email_subject'} instead.
*
* @param string $subject The email subject.
* @param string $sitename The name of the site.
* @param array $email_data {
* Data relating to the account action email.
*
* @type WP_User_Request $request User request object.
* @type string $message_recipient The address that the email will be sent to. Defaults
* to the value of `$request->email`, but can be changed
* by the `user_erasure_fulfillment_email_to` filter.
* @type string $privacy_policy_url Privacy policy URL.
* @type string $sitename The site name sending the mail.
* @type string $siteurl The site URL sending the mail.
* }
*/
$subject = apply_filters_deprecated(
'user_erasure_complete_email_subject',
array( $subject, $email_data['sitename'], $email_data ),
'5.8.0',
'user_erasure_fulfillment_email_subject'
);
/**
* Filters the subject of the email sent when an erasure request is completed.
*
* @since 5.8.0
*
* @param string $subject The email subject.
* @param string $sitename The name of the site.
* @param array $email_data {
* Data relating to the account action email.
*
* @type WP_User_Request $request User request object.
* @type string $message_recipient The address that the email will be sent to. Defaults
* to the value of `$request->email`, but can be changed
* by the `user_erasure_fulfillment_email_to` filter.
* @type string $privacy_policy_url Privacy policy URL.
* @type string $sitename The site name sending the mail.
* @type string $siteurl The site URL sending the mail.
* }
*/
$subject = apply_filters( 'user_erasure_fulfillment_email_subject', $subject, $email_data['sitename'], $email_data );
/* translators: Do not translate SITENAME, SITEURL; those are placeholders. */
$content = __(
'Howdy,
Your request to erase your personal data on ###SITENAME### has been completed.
If you have any follow-up questions or concerns, please contact the site administrator.
Regards,
All at ###SITENAME###
###SITEURL###'
);
if ( ! empty( $email_data['privacy_policy_url'] ) ) {
/* translators: Do not translate SITENAME, SITEURL, PRIVACY_POLICY_URL; those are placeholders. */
$content = __(
'Howdy,
Your request to erase your personal data on ###SITENAME### has been completed.
If you have any follow-up questions or concerns, please contact the site administrator.
For more information, you can also read our privacy policy: ###PRIVACY_POLICY_URL###
Regards,
All at ###SITENAME###
###SITEURL###'
);
}
/**
* Filters the body of the data erasure fulfillment notification.
*
* The email is sent to a user when their data erasure request is fulfilled
* by an administrator.
*
* The following strings have a special meaning and will get replaced dynamically:
*
* ###SITENAME### The name of the site.
* ###PRIVACY_POLICY_URL### Privacy policy page URL.
* ###SITEURL### The URL to the site.
*
* @since 4.9.6
* @deprecated 5.8.0 Use {@see 'user_erasure_fulfillment_email_content'} instead.
* For user request confirmation email content
* use {@see 'user_request_confirmed_email_content'} instead.
*
* @param string $content The email content.
* @param array $email_data {
* Data relating to the account action email.
*
* @type WP_User_Request $request User request object.
* @type string $message_recipient The address that the email will be sent to. Defaults
* to the value of `$request->email`, but can be changed
* by the `user_erasure_fulfillment_email_to` filter.
* @type string $privacy_policy_url Privacy policy URL.
* @type string $sitename The site name sending the mail.
* @type string $siteurl The site URL sending the mail.
* }
*/
$content = apply_filters_deprecated(
'user_confirmed_action_email_content',
array( $content, $email_data ),
'5.8.0',
sprintf(
/* translators: 1 & 2: Deprecation replacement options. */
__( '%1$s or %2$s' ),
'user_erasure_fulfillment_email_content',
'user_request_confirmed_email_content'
)
);
/**
* Filters the body of the data erasure fulfillment notification.
*
* The email is sent to a user when their data erasure request is fulfilled
* by an administrator.
*
* The following strings have a special meaning and will get replaced dynamically:
*
* ###SITENAME### The name of the site.
* ###PRIVACY_POLICY_URL### Privacy policy page URL.
* ###SITEURL### The URL to the site.
*
* @since 5.8.0
*
* @param string $content The email content.
* @param array $email_data {
* Data relating to the account action email.
*
* @type WP_User_Request $request User request object.
* @type string $message_recipient The address that the email will be sent to. Defaults
* to the value of `$request->email`, but can be changed
* by the `user_erasure_fulfillment_email_to` filter.
* @type string $privacy_policy_url Privacy policy URL.
* @type string $sitename The site name sending the mail.
* @type string $siteurl The site URL sending the mail.
* }
*/
$content = apply_filters( 'user_erasure_fulfillment_email_content', $content, $email_data );
$content = str_replace( '###SITENAME###', $email_data['sitename'], $content );
$content = str_replace( '###PRIVACY_POLICY_URL###', $email_data['privacy_policy_url'], $content );
$content = str_replace( '###SITEURL###', sanitize_url( $email_data['siteurl'] ), $content );
$headers = '';
/**
* Filters the headers of the data erasure fulfillment notification.
*
* @since 5.4.0
* @deprecated 5.8.0 Use {@see 'user_erasure_fulfillment_email_headers'} instead.
*
* @param string|array $headers The email headers.
* @param string $subject The email subject.
* @param string $content The email content.
* @param int $request_id The request ID.
* @param array $email_data {
* Data relating to the account action email.
*
* @type WP_User_Request $request User request object.
* @type string $message_recipient The address that the email will be sent to. Defaults
* to the value of `$request->email`, but can be changed
* by the `user_erasure_fulfillment_email_to` filter.
* @type string $privacy_policy_url Privacy policy URL.
* @type string $sitename The site name sending the mail.
* @type string $siteurl The site URL sending the mail.
* }
*/
$headers = apply_filters_deprecated(
'user_erasure_complete_email_headers',
array( $headers, $subject, $content, $request_id, $email_data ),
'5.8.0',
'user_erasure_fulfillment_email_headers'
);
/**
* Filters the headers of the data erasure fulfillment notification.
*
* @since 5.8.0
*
* @param string|array $headers The email headers.
* @param string $subject The email subject.
* @param string $content The email content.
* @param int $request_id The request ID.
* @param array $email_data {
* Data relating to the account action email.
*
* @type WP_User_Request $request User request object.
* @type string $message_recipient The address that the email will be sent to. Defaults
* to the value of `$request->email`, but can be changed
* by the `user_erasure_fulfillment_email_to` filter.
* @type string $privacy_policy_url Privacy policy URL.
* @type string $sitename The site name sending the mail.
* @type string $siteurl The site URL sending the mail.
* }
*/
$headers = apply_filters( 'user_erasure_fulfillment_email_headers', $headers, $subject, $content, $request_id, $email_data );
$email_sent = wp_mail( $user_email, $subject, $content, $headers );
if ( $switched_locale ) {
restore_previous_locale();
}
if ( $email_sent ) {
update_post_meta( $request_id, '_wp_user_notified', true );
}
}
/**
* Returns request confirmation message HTML.
*
* @since 4.9.6
* @access private
*
* @param int $request_id The request ID being confirmed.
* @return string The confirmation message.
*/
function _wp_privacy_account_request_confirmed_message( $request_id ) {
$request = wp_get_user_request( $request_id );
$message = '' . __( 'Action has been confirmed.' ) . '
'; $message .= '' . __( 'The site administrator has been notified and will fulfill your request as soon as possible.' ) . '
'; if ( $request && in_array( $request->action_name, _wp_privacy_action_request_types(), true ) ) { if ( 'export_personal_data' === $request->action_name ) { $message = '' . __( 'Thanks for confirming your export request.' ) . '
'; $message .= '' . __( 'The site administrator has been notified. You will receive a link to download your export via email when they fulfill your request.' ) . '
'; } elseif ( 'remove_personal_data' === $request->action_name ) { $message = '' . __( 'Thanks for confirming your erasure request.' ) . '
'; $message .= '' . __( 'The site administrator has been notified. You will receive an email confirmation when they erase your data.' ) . '
'; } } /** * Filters the message displayed to a user when they confirm a data request. * * @since 4.9.6 * * @param string $message The message to the user. * @param int $request_id The ID of the request being confirmed. */ $message = apply_filters( 'user_request_action_confirmed_message', $message, $request_id ); return $message; } /** * Creates and logs a user request to perform a specific action. * * Requests are stored inside a post type named `user_request` since they can apply to both * users on the site, or guests without a user account. * * @since 4.9.6 * @since 5.7.0 Added the `$status` parameter. * * @param string $email_address User email address. This can be the address of a registered * or non-registered user. * @param string $action_name Name of the action that is being confirmed. Required. * @param array $request_data Misc data you want to send with the verification request and pass * to the actions once the request is confirmed. * @param string $status Optional request status (pending or confirmed). Default 'pending'. * @return int|WP_Error Returns the request ID if successful, or a WP_Error object on failure. */ function wp_create_user_request( $email_address = '', $action_name = '', $request_data = array(), $status = 'pending' ) { $email_address = sanitize_email( $email_address ); $action_name = sanitize_key( $action_name ); if ( ! is_email( $email_address ) ) { return new WP_Error( 'invalid_email', __( 'Invalid email address.' ) ); } if ( ! in_array( $action_name, _wp_privacy_action_request_types(), true ) ) { return new WP_Error( 'invalid_action', __( 'Invalid action name.' ) ); } if ( ! in_array( $status, array( 'pending', 'confirmed' ), true ) ) { return new WP_Error( 'invalid_status', __( 'Invalid request status.' ) ); } $user = get_user_by( 'email', $email_address ); $user_id = $user && ! is_wp_error( $user ) ? $user->ID : 0; // Check for duplicates. $requests_query = new WP_Query( array( 'post_type' => 'user_request', 'post_name__in' => array( $action_name ), // Action name stored in post_name column. 'title' => $email_address, // Email address stored in post_title column. 'post_status' => array( 'request-pending', 'request-confirmed', ), 'fields' => 'ids', ) ); if ( $requests_query->found_posts ) { return new WP_Error( 'duplicate_request', __( 'An incomplete personal data request for this email address already exists.' ) ); } $request_id = wp_insert_post( array( 'post_author' => $user_id, 'post_name' => $action_name, 'post_title' => $email_address, 'post_content' => wp_json_encode( $request_data ), 'post_status' => 'request-' . $status, 'post_type' => 'user_request', 'post_date' => current_time( 'mysql', false ), 'post_date_gmt' => current_time( 'mysql', true ), ), true ); return $request_id; } /** * Gets action description from the name and return a string. * * @since 4.9.6 * * @param string $action_name Action name of the request. * @return string Human readable action name. */ function wp_user_request_action_description( $action_name ) { switch ( $action_name ) { case 'export_personal_data': $description = __( 'Export Personal Data' ); break; case 'remove_personal_data': $description = __( 'Erase Personal Data' ); break; default: /* translators: %s: Action name. */ $description = sprintf( __( 'Confirm the "%s" action' ), $action_name ); break; } /** * Filters the user action description. * * @since 4.9.6 * * @param string $description The default description. * @param string $action_name The name of the request. */ return apply_filters( 'user_request_action_description', $description, $action_name ); } /** * Send a confirmation request email to confirm an action. * * If the request is not already pending, it will be updated. * * @since 4.9.6 * * @param string $request_id ID of the request created via wp_create_user_request(). * @return true|WP_Error True on success, `WP_Error` on failure. */ function wp_send_user_request( $request_id ) { $request_id = absint( $request_id ); $request = wp_get_user_request( $request_id ); if ( ! $request ) { return new WP_Error( 'invalid_request', __( 'Invalid personal data request.' ) ); } // Localize message content for user; fallback to site default for visitors. if ( ! empty( $request->user_id ) ) { $switched_locale = switch_to_user_locale( $request->user_id ); } else { $switched_locale = switch_to_locale( get_locale() ); } $email_data = array( 'request' => $request, 'email' => $request->email, 'description' => wp_user_request_action_description( $request->action_name ), 'confirm_url' => add_query_arg( array( 'action' => 'confirmaction', 'request_id' => $request_id, 'confirm_key' => wp_generate_user_request_key( $request_id ), ), wp_login_url() ), 'sitename' => wp_specialchars_decode( get_option( 'blogname' ), ENT_QUOTES ), 'siteurl' => home_url(), ); /* translators: Confirm privacy data request notification email subject. 1: Site title, 2: Name of the action. */ $subject = sprintf( __( '[%1$s] Confirm Action: %2$s' ), $email_data['sitename'], $email_data['description'] ); /** * Filters the subject of the email sent when an account action is attempted. * * @since 4.9.6 * * @param string $subject The email subject. * @param string $sitename The name of the site. * @param array $email_data { * Data relating to the account action email. * * @type WP_User_Request $request User request object. * @type string $email The email address this is being sent to. * @type string $description Description of the action being performed so the user knows what the email is for. * @type string $confirm_url The link to click on to confirm the account action. * @type string $sitename The site name sending the mail. * @type string $siteurl The site URL sending the mail. * } */ $subject = apply_filters( 'user_request_action_email_subject', $subject, $email_data['sitename'], $email_data ); /* translators: Do not translate DESCRIPTION, CONFIRM_URL, SITENAME, SITEURL: those are placeholders. */ $content = __( 'Howdy, A request has been made to perform the following action on your account: ###DESCRIPTION### To confirm this, please click on the following link: ###CONFIRM_URL### You can safely ignore and delete this email if you do not want to take this action. Regards, All at ###SITENAME### ###SITEURL###' ); /** * Filters the text of the email sent when an account action is attempted. * * The following strings have a special meaning and will get replaced dynamically: * * ###DESCRIPTION### Description of the action being performed so the user knows what the email is for. * ###CONFIRM_URL### The link to click on to confirm the account action. * ###SITENAME### The name of the site. * ###SITEURL### The URL to the site. * * @since 4.9.6 * * @param string $content Text in the email. * @param array $email_data { * Data relating to the account action email. * * @type WP_User_Request $request User request object. * @type string $email The email address this is being sent to. * @type string $description Description of the action being performed so the user knows what the email is for. * @type string $confirm_url The link to click on to confirm the account action. * @type string $sitename The site name sending the mail. * @type string $siteurl The site URL sending the mail. * } */ $content = apply_filters( 'user_request_action_email_content', $content, $email_data ); $content = str_replace( '###DESCRIPTION###', $email_data['description'], $content ); $content = str_replace( '###CONFIRM_URL###', sanitize_url( $email_data['confirm_url'] ), $content ); $content = str_replace( '###EMAIL###', $email_data['email'], $content ); $content = str_replace( '###SITENAME###', $email_data['sitename'], $content ); $content = str_replace( '###SITEURL###', sanitize_url( $email_data['siteurl'] ), $content ); $headers = ''; /** * Filters the headers of the email sent when an account action is attempted. * * @since 5.4.0 * * @param string|array $headers The email headers. * @param string $subject The email subject. * @param string $content The email content. * @param int $request_id The request ID. * @param array $email_data { * Data relating to the account action email. * * @type WP_User_Request $request User request object. * @type string $email The email address this is being sent to. * @type string $description Description of the action being performed so the user knows what the email is for. * @type string $confirm_url The link to click on to confirm the account action. * @type string $sitename The site name sending the mail. * @type string $siteurl The site URL sending the mail. * } */ $headers = apply_filters( 'user_request_action_email_headers', $headers, $subject, $content, $request_id, $email_data ); $email_sent = wp_mail( $email_data['email'], $subject, $content, $headers ); if ( $switched_locale ) { restore_previous_locale(); } if ( ! $email_sent ) { return new WP_Error( 'privacy_email_error', __( 'Unable to send personal data export confirmation email.' ) ); } return true; } /** * Returns a confirmation key for a user action and stores the hashed version for future comparison. * * @since 4.9.6 * * @param int $request_id Request ID. * @return string Confirmation key. */ function wp_generate_user_request_key( $request_id ) { global $wp_hasher; // Generate something random for a confirmation key. $key = wp_generate_password( 20, false ); // Return the key, hashed. if ( empty( $wp_hasher ) ) { require_once ABSPATH . WPINC . '/class-phpass.php'; $wp_hasher = new PasswordHash( 8, true ); } wp_update_post( array( 'ID' => $request_id, 'post_status' => 'request-pending', 'post_password' => $wp_hasher->HashPassword( $key ), ) ); return $key; } /** * Validates a user request by comparing the key with the request's key. * * @since 4.9.6 * * @param string $request_id ID of the request being confirmed. * @param string $key Provided key to validate. * @return true|WP_Error True on success, WP_Error on failure. */ function wp_validate_user_request_key( $request_id, $key ) { global $wp_hasher; $request_id = absint( $request_id ); $request = wp_get_user_request( $request_id ); $saved_key = $request->confirm_key; $key_request_time = $request->modified_timestamp; if ( ! $request || ! $saved_key || ! $key_request_time ) { return new WP_Error( 'invalid_request', __( 'Invalid personal data request.' ) ); } if ( ! in_array( $request->status, array( 'request-pending', 'request-failed' ), true ) ) { return new WP_Error( 'expired_request', __( 'This personal data request has expired.' ) ); } if ( empty( $key ) ) { return new WP_Error( 'missing_key', __( 'The confirmation key is missing from this personal data request.' ) ); } if ( empty( $wp_hasher ) ) { require_once ABSPATH . WPINC . '/class-phpass.php'; $wp_hasher = new PasswordHash( 8, true ); } /** * Filters the expiration time of confirm keys. * * @since 4.9.6 * * @param int $expiration The expiration time in seconds. */ $expiration_duration = (int) apply_filters( 'user_request_key_expiration', DAY_IN_SECONDS ); $expiration_time = $key_request_time + $expiration_duration; if ( ! $wp_hasher->CheckPassword( $key, $saved_key ) ) { return new WP_Error( 'invalid_key', __( 'The confirmation key is invalid for this personal data request.' ) ); } if ( ! $expiration_time || time() > $expiration_time ) { return new WP_Error( 'expired_key', __( 'The confirmation key has expired for this personal data request.' ) ); } return true; } /** * Returns the user request object for the specified request ID. * * @since 4.9.6 * * @param int $request_id The ID of the user request. * @return WP_User_Request|false */ function wp_get_user_request( $request_id ) { $request_id = absint( $request_id ); $post = get_post( $request_id ); if ( ! $post || 'user_request' !== $post->post_type ) { return false; } return new WP_User_Request( $post ); } /** * Checks if Application Passwords is supported. * * Application Passwords is supported only by sites using SSL or local environments * but may be made available using the {@see 'wp_is_application_passwords_available'} filter. * * @since 5.9.0 * * @return bool */ function wp_is_application_passwords_supported() { return is_ssl() || 'local' === wp_get_environment_type(); } /** * Checks if Application Passwords is globally available. * * By default, Application Passwords is available to all sites using SSL or to local environments. * Use the {@see 'wp_is_application_passwords_available'} filter to adjust its availability. * * @since 5.6.0 * * @return bool */ function wp_is_application_passwords_available() { /** * Filters whether Application Passwords is available. * * @since 5.6.0 * * @param bool $available True if available, false otherwise. */ return apply_filters( 'wp_is_application_passwords_available', wp_is_application_passwords_supported() ); } /** * Checks if Application Passwords is available for a specific user. * * By default all users can use Application Passwords. Use {@see 'wp_is_application_passwords_available_for_user'} * to restrict availability to certain users. * * @since 5.6.0 * * @param int|WP_User $user The user to check. * @return bool */ function wp_is_application_passwords_available_for_user( $user ) { if ( ! wp_is_application_passwords_available() ) { return false; } if ( ! is_object( $user ) ) { $user = get_userdata( $user ); } if ( ! $user || ! $user->exists() ) { return false; } /** * Filters whether Application Passwords is available for a specific user. * * @since 5.6.0 * * @param bool $available True if available, false otherwise. * @param WP_User $user The user to check. */ return apply_filters( 'wp_is_application_passwords_available_for_user', true, $user ); } /** * Registers the user meta property for persisted preferences. * * This property is used to store user preferences across page reloads and is * currently used by the block editor for preferences like 'fullscreenMode' and * 'fixedToolbar'. * * @since 6.1.0 * @access private * * @global wpdb $wpdb WordPress database abstraction object. */ function wp_register_persisted_preferences_meta() { /* * Create a meta key that incorporates the blog prefix so that each site * on a multisite can have distinct user preferences. */ global $wpdb; $meta_key = $wpdb->get_blog_prefix() . 'persisted_preferences'; register_meta( 'user', $meta_key, array( 'type' => 'object', 'single' => true, 'show_in_rest' => array( 'name' => 'persisted_preferences', 'type' => 'object', 'schema' => array( 'type' => 'object', 'context' => array( 'edit' ), 'properties' => array( '_modified' => array( 'description' => __( 'The date and time the preferences were updated.' ), 'type' => 'string', 'format' => 'date-time', 'readonly' => false, ), ), 'additionalProperties' => true, ), ), ) ); } /** * Sets the last changed time for the 'users' cache group. * * @since 6.3.0 */ function wp_cache_set_users_last_changed() { wp_cache_set_last_changed( 'users' ); } /** * Checks if password reset is allowed for a specific user. * * @since 6.3.0 * * @param int|WP_User $user The user to check. * @return bool|WP_Error True if allowed, false or WP_Error otherwise. */ function wp_is_password_reset_allowed_for_user( $user ) { if ( ! is_object( $user ) ) { $user = get_userdata( $user ); } if ( ! $user || ! $user->exists() ) { return false; } $allow = true; if ( is_multisite() && is_user_spammy( $user ) ) { $allow = false; } /** * Filters whether to allow a password to be reset. * * @since 2.7.0 * * @param bool $allow Whether to allow the password to be reset. Default true. * @param int $user_id The ID of the user attempting to reset a password. */ return apply_filters( 'allow_password_reset', $allow, $user->ID ); } onomy object on success, WP_Error object on failure. */ function register_taxonomy( $taxonomy, $object_type, $args = array() ) { global $wp_taxonomies; if ( ! is_array( $wp_taxonomies ) ) { $wp_taxonomies = array(); } $args = wp_parse_args( $args ); if ( empty( $taxonomy ) || strlen( $taxonomy ) > 32 ) { _doing_it_wrong( __FUNCTION__, __( 'Taxonomy names must be between 1 and 32 characters in length.' ), '4.2.0' ); return new WP_Error( 'taxonomy_length_invalid', __( 'Taxonomy names must be between 1 and 32 characters in length.' ) ); } $taxonomy_object = new WP_Taxonomy( $taxonomy, $object_type, $args ); $taxonomy_object->add_rewrite_rules(); $wp_taxonomies[ $taxonomy ] = $taxonomy_object; $taxonomy_object->add_hooks(); // Add default term. if ( ! empty( $taxonomy_object->default_term ) ) { $term = term_exists( $taxonomy_object->default_term['name'], $taxonomy ); if ( $term ) { update_option( 'default_term_' . $taxonomy_object->name, $term['term_id'] ); } else { $term = wp_insert_term( $taxonomy_object->default_term['name'], $taxonomy, array( 'slug' => sanitize_title( $taxonomy_object->default_term['slug'] ), 'description' => $taxonomy_object->default_term['description'], ) ); // Update `term_id` in options. if ( ! is_wp_error( $term ) ) { update_option( 'default_term_' . $taxonomy_object->name, $term['term_id'] ); } } } /** * Fires after a taxonomy is registered. * * @since 3.3.0 * * @param string $taxonomy Taxonomy slug. * @param array|string $object_type Object type or array of object types. * @param array $args Array of taxonomy registration arguments. */ do_action( 'registered_taxonomy', $taxonomy, $object_type, (array) $taxonomy_object ); /** * Fires after a specific taxonomy is registered. * * The dynamic portion of the filter name, `$taxonomy`, refers to the taxonomy key. * * Possible hook names include: * * - `registered_taxonomy_category` * - `registered_taxonomy_post_tag` * * @since 6.0.0 * * @param string $taxonomy Taxonomy slug. * @param array|string $object_type Object type or array of object types. * @param array $args Array of taxonomy registration arguments. */ do_action( "registered_taxonomy_{$taxonomy}", $taxonomy, $object_type, (array) $taxonomy_object ); return $taxonomy_object; } /** * Unregisters a taxonomy. * * Can not be used to unregister built-in taxonomies. * * @since 4.5.0 * * @global WP_Taxonomy[] $wp_taxonomies List of taxonomies. * * @param string $taxonomy Taxonomy name. * @return true|WP_Error True on success, WP_Error on failure or if the taxonomy doesn't exist. */ function unregister_taxonomy( $taxonomy ) { global $wp_taxonomies; if ( ! taxonomy_exists( $taxonomy ) ) { return new WP_Error( 'invalid_taxonomy', __( 'Invalid taxonomy.' ) ); } $taxonomy_object = get_taxonomy( $taxonomy ); // Do not allow unregistering internal taxonomies. if ( $taxonomy_object->_builtin ) { return new WP_Error( 'invalid_taxonomy', __( 'Unregistering a built-in taxonomy is not allowed.' ) ); } $taxonomy_object->remove_rewrite_rules(); $taxonomy_object->remove_hooks(); // Remove the taxonomy. unset( $wp_taxonomies[ $taxonomy ] ); /** * Fires after a taxonomy is unregistered. * * @since 4.5.0 * * @param string $taxonomy Taxonomy name. */ do_action( 'unregistered_taxonomy', $taxonomy ); return true; } /** * Builds an object with all taxonomy labels out of a taxonomy object. * * @since 3.0.0 * @since 4.3.0 Added the `no_terms` label. * @since 4.4.0 Added the `items_list_navigation` and `items_list` labels. * @since 4.9.0 Added the `most_used` and `back_to_items` labels. * @since 5.7.0 Added the `filter_by_item` label. * @since 5.8.0 Added the `item_link` and `item_link_description` labels. * @since 5.9.0 Added the `name_field_description`, `slug_field_description`, * `parent_field_description`, and `desc_field_description` labels. * * @param WP_Taxonomy $tax Taxonomy object. * @return object { * Taxonomy labels object. The first default value is for non-hierarchical taxonomies * (like tags) and the second one is for hierarchical taxonomies (like categories). * * @type string $name General name for the taxonomy, usually plural. The same * as and overridden by `$tax->label`. Default 'Tags'/'Categories'. * @type string $singular_name Name for one object of this taxonomy. Default 'Tag'/'Category'. * @type string $search_items Default 'Search Tags'/'Search Categories'. * @type string $popular_items This label is only used for non-hierarchical taxonomies. * Default 'Popular Tags'. * @type string $all_items Default 'All Tags'/'All Categories'. * @type string $parent_item This label is only used for hierarchical taxonomies. Default * 'Parent Category'. * @type string $parent_item_colon The same as `parent_item`, but with colon `:` in the end. * @type string $name_field_description Description for the Name field on Edit Tags screen. * Default 'The name is how it appears on your site'. * @type string $slug_field_description Description for the Slug field on Edit Tags screen. * Default 'The “slug” is the URL-friendly version * of the name. It is usually all lowercase and contains * only letters, numbers, and hyphens'. * @type string $parent_field_description Description for the Parent field on Edit Tags screen. * Default 'Assign a parent term to create a hierarchy. * The term Jazz, for example, would be the parent * of Bebop and Big Band'. * @type string $desc_field_description Description for the Description field on Edit Tags screen. * Default 'The description is not prominent by default; * however, some themes may show it'. * @type string $edit_item Default 'Edit Tag'/'Edit Category'. * @type string $view_item Default 'View Tag'/'View Category'. * @type string $update_item Default 'Update Tag'/'Update Category'. * @type string $add_new_item Default 'Add New Tag'/'Add New Category'. * @type string $new_item_name Default 'New Tag Name'/'New Category Name'. * @type string $separate_items_with_commas This label is only used for non-hierarchical taxonomies. Default * 'Separate tags with commas', used in the meta box. * @type string $add_or_remove_items This label is only used for non-hierarchical taxonomies. Default * 'Add or remove tags', used in the meta box when JavaScript * is disabled. * @type string $choose_from_most_used This label is only used on non-hierarchical taxonomies. Default * 'Choose from the most used tags', used in the meta box. * @type string $not_found Default 'No tags found'/'No categories found', used in * the meta box and taxonomy list table. * @type string $no_terms Default 'No tags'/'No categories', used in the posts and media * list tables. * @type string $filter_by_item This label is only used for hierarchical taxonomies. Default * 'Filter by category', used in the posts list table. * @type string $items_list_navigation Label for the table pagination hidden heading. * @type string $items_list Label for the table hidden heading. * @type string $most_used Title for the Most Used tab. Default 'Most Used'. * @type string $back_to_items Label displayed after a term has been updated. * @type string $item_link Used in the block editor. Title for a navigation link block variation. * Default 'Tag Link'/'Category Link'. * @type string $item_link_description Used in the block editor. Description for a navigation link block * variation. Default 'A link to a tag'/'A link to a category'. * } */ function get_taxonomy_labels( $tax ) { $tax->labels = (array) $tax->labels; if ( isset( $tax->helps ) && empty( $tax->labels['separate_items_with_commas'] ) ) { $tax->labels['separate_items_with_commas'] = $tax->helps; } if ( isset( $tax->no_tagcloud ) && empty( $tax->labels['not_found'] ) ) { $tax->labels['not_found'] = $tax->no_tagcloud; } $nohier_vs_hier_defaults = WP_Taxonomy::get_default_labels(); $nohier_vs_hier_defaults['menu_name'] = $nohier_vs_hier_defaults['name']; $labels = _get_custom_object_labels( $tax, $nohier_vs_hier_defaults ); $taxonomy = $tax->name; $default_labels = clone $labels; /** * Filters the labels of a specific taxonomy. * * The dynamic portion of the hook name, `$taxonomy`, refers to the taxonomy slug. * * Possible hook names include: * * - `taxonomy_labels_category` * - `taxonomy_labels_post_tag` * * @since 4.4.0 * * @see get_taxonomy_labels() for the full list of taxonomy labels. * * @param object $labels Object with labels for the taxonomy as member variables. */ $labels = apply_filters( "taxonomy_labels_{$taxonomy}", $labels ); // Ensure that the filtered labels contain all required default values. $labels = (object) array_merge( (array) $default_labels, (array) $labels ); return $labels; } /** * Adds an already registered taxonomy to an object type. * * @since 3.0.0 * * @global WP_Taxonomy[] $wp_taxonomies The registered taxonomies. * * @param string $taxonomy Name of taxonomy object. * @param string $object_type Name of the object type. * @return bool True if successful, false if not. */ function register_taxonomy_for_object_type( $taxonomy, $object_type ) { global $wp_taxonomies; if ( ! isset( $wp_taxonomies[ $taxonomy ] ) ) { return false; } if ( ! get_post_type_object( $object_type ) ) { return false; } if ( ! in_array( $object_type, $wp_taxonomies[ $taxonomy ]->object_type, true ) ) { $wp_taxonomies[ $taxonomy ]->object_type[] = $object_type; } // Filter out empties. $wp_taxonomies[ $taxonomy ]->object_type = array_filter( $wp_taxonomies[ $taxonomy ]->object_type ); /** * Fires after a taxonomy is registered for an object type. * * @since 5.1.0 * * @param string $taxonomy Taxonomy name. * @param string $object_type Name of the object type. */ do_action( 'registered_taxonomy_for_object_type', $taxonomy, $object_type ); return true; } /** * Removes an already registered taxonomy from an object type. * * @since 3.7.0 * * @global WP_Taxonomy[] $wp_taxonomies The registered taxonomies. * * @param string $taxonomy Name of taxonomy object. * @param string $object_type Name of the object type. * @return bool True if successful, false if not. */ function unregister_taxonomy_for_object_type( $taxonomy, $object_type ) { global $wp_taxonomies; if ( ! isset( $wp_taxonomies[ $taxonomy ] ) ) { return false; } if ( ! get_post_type_object( $object_type ) ) { return false; } $key = array_search( $object_type, $wp_taxonomies[ $taxonomy ]->object_type, true ); if ( false === $key ) { return false; } unset( $wp_taxonomies[ $taxonomy ]->object_type[ $key ] ); /** * Fires after a taxonomy is unregistered for an object type. * * @since 5.1.0 * * @param string $taxonomy Taxonomy name. * @param string $object_type Name of the object type. */ do_action( 'unregistered_taxonomy_for_object_type', $taxonomy, $object_type ); return true; } // // Term API. // /** * Retrieves object IDs of valid taxonomy and term. * * The strings of `$taxonomies` must exist before this function will continue. * On failure of finding a valid taxonomy, it will return a WP_Error. * * The `$terms` aren't checked the same as `$taxonomies`, but still need to exist * for object IDs to be returned. * * It is possible to change the order that object IDs are returned by using `$args` * with either ASC or DESC array. The value should be in the key named 'order'. * * @since 2.3.0 * * @global wpdb $wpdb WordPress database abstraction object. * * @param int|int[] $term_ids Term ID or array of term IDs of terms that will be used. * @param string|string[] $taxonomies String of taxonomy name or Array of string values of taxonomy names. * @param array|string $args { * Change the order of the object IDs. * * @type string $order Order to retrieve terms. Accepts 'ASC' or 'DESC'. Default 'ASC'. * } * @return string[]|WP_Error An array of object IDs as numeric strings on success, * WP_Error if the taxonomy does not exist. */ function get_objects_in_term( $term_ids, $taxonomies, $args = array() ) { global $wpdb; if ( ! is_array( $term_ids ) ) { $term_ids = array( $term_ids ); } if ( ! is_array( $taxonomies ) ) { $taxonomies = array( $taxonomies ); } foreach ( (array) $taxonomies as $taxonomy ) { if ( ! taxonomy_exists( $taxonomy ) ) { return new WP_Error( 'invalid_taxonomy', __( 'Invalid taxonomy.' ) ); } } $defaults = array( 'order' => 'ASC' ); $args = wp_parse_args( $args, $defaults ); $order = ( 'desc' === strtolower( $args['order'] ) ) ? 'DESC' : 'ASC'; $term_ids = array_map( 'intval', $term_ids ); $taxonomies = "'" . implode( "', '", array_map( 'esc_sql', $taxonomies ) ) . "'"; $term_ids = "'" . implode( "', '", $term_ids ) . "'"; $sql = "SELECT tr.object_id FROM $wpdb->term_relationships AS tr INNER JOIN $wpdb->term_taxonomy AS tt ON tr.term_taxonomy_id = tt.term_taxonomy_id WHERE tt.taxonomy IN ($taxonomies) AND tt.term_id IN ($term_ids) ORDER BY tr.object_id $order"; $last_changed = wp_cache_get_last_changed( 'terms' ); $cache_key = 'get_objects_in_term:' . md5( $sql ) . ":$last_changed"; $cache = wp_cache_get( $cache_key, 'term-queries' ); if ( false === $cache ) { $object_ids = $wpdb->get_col( $sql ); wp_cache_set( $cache_key, $object_ids, 'term-queries' ); } else { $object_ids = (array) $cache; } if ( ! $object_ids ) { return array(); } return $object_ids; } /** * Given a taxonomy query, generates SQL to be appended to a main query. * * @since 3.1.0 * * @see WP_Tax_Query * * @param array $tax_query A compact tax query * @param string $primary_table * @param string $primary_id_column * @return string[] */ function get_tax_sql( $tax_query, $primary_table, $primary_id_column ) { $tax_query_obj = new WP_Tax_Query( $tax_query ); return $tax_query_obj->get_sql( $primary_table, $primary_id_column ); } /** * Gets all term data from database by term ID. * * The usage of the get_term function is to apply filters to a term object. It * is possible to get a term object from the database before applying the * filters. * * $term ID must be part of $taxonomy, to get from the database. Failure, might * be able to be captured by the hooks. Failure would be the same value as $wpdb * returns for the get_row method. * * There are two hooks, one is specifically for each term, named 'get_term', and * the second is for the taxonomy name, 'term_$taxonomy'. Both hooks gets the * term object, and the taxonomy name as parameters. Both hooks are expected to * return a term object. * * {@see 'get_term'} hook - Takes two parameters the term Object and the taxonomy name. * Must return term object. Used in get_term() as a catch-all filter for every * $term. * * {@see 'get_$taxonomy'} hook - Takes two parameters the term Object and the taxonomy * name. Must return term object. $taxonomy will be the taxonomy name, so for * example, if 'category', it would be 'get_category' as the filter name. Useful * for custom taxonomies or plugging into default taxonomies. * * @todo Better formatting for DocBlock * * @since 2.3.0 * @since 4.4.0 Converted to return a WP_Term object if `$output` is `OBJECT`. * The `$taxonomy` parameter was made optional. * * @see sanitize_term_field() The $context param lists the available values for get_term_by() $filter param. * * @param int|WP_Term|object $term If integer, term data will be fetched from the database, * or from the cache if available. * If stdClass object (as in the results of a database query), * will apply filters and return a `WP_Term` object with the `$term` data. * If `WP_Term`, will return `$term`. * @param string $taxonomy Optional. Taxonomy name that `$term` is part of. * @param string $output Optional. The required return type. One of OBJECT, ARRAY_A, or ARRAY_N, which * correspond to a WP_Term object, an associative array, or a numeric array, * respectively. Default OBJECT. * @param string $filter Optional. How to sanitize term fields. Default 'raw'. * @return WP_Term|array|WP_Error|null WP_Term instance (or array) on success, depending on the `$output` value. * WP_Error if `$taxonomy` does not exist. Null for miscellaneous failure. */ function get_term( $term, $taxonomy = '', $output = OBJECT, $filter = 'raw' ) { if ( empty( $term ) ) { return new WP_Error( 'invalid_term', __( 'Empty Term.' ) ); } if ( $taxonomy && ! taxonomy_exists( $taxonomy ) ) { return new WP_Error( 'invalid_taxonomy', __( 'Invalid taxonomy.' ) ); } if ( $term instanceof WP_Term ) { $_term = $term; } elseif ( is_object( $term ) ) { if ( empty( $term->filter ) || 'raw' === $term->filter ) { $_term = sanitize_term( $term, $taxonomy, 'raw' ); $_term = new WP_Term( $_term ); } else { $_term = WP_Term::get_instance( $term->term_id ); } } else { $_term = WP_Term::get_instance( $term, $taxonomy ); } if ( is_wp_error( $_term ) ) { return $_term; } elseif ( ! $_term ) { return null; } // Ensure for filters that this is not empty. $taxonomy = $_term->taxonomy; $old_term = $_term; /** * Filters a taxonomy term object. * * The {@see 'get_$taxonomy'} hook is also available for targeting a specific * taxonomy. * * @since 2.3.0 * @since 4.4.0 `$_term` is now a `WP_Term` object. * * @param WP_Term $_term Term object. * @param string $taxonomy The taxonomy slug. */ $_term = apply_filters( 'get_term', $_term, $taxonomy ); /** * Filters a taxonomy term object. * * The dynamic portion of the hook name, `$taxonomy`, refers * to the slug of the term's taxonomy. * * Possible hook names include: * * - `get_category` * - `get_post_tag` * * @since 2.3.0 * @since 4.4.0 `$_term` is now a `WP_Term` object. * * @param WP_Term $_term Term object. * @param string $taxonomy The taxonomy slug. */ $_term = apply_filters( "get_{$taxonomy}", $_term, $taxonomy ); // Bail if a filter callback has changed the type of the `$_term` object. if ( ! ( $_term instanceof WP_Term ) ) { return $_term; } // Sanitize term, according to the specified filter. if ( $_term !== $old_term || $_term->filter !== $filter ) { $_term->filter( $filter ); } if ( ARRAY_A === $output ) { return $_term->to_array(); } elseif ( ARRAY_N === $output ) { return array_values( $_term->to_array() ); } return $_term; } /** * Gets all term data from database by term field and data. * * Warning: $value is not escaped for 'name' $field. You must do it yourself, if * required. * * The default $field is 'id', therefore it is possible to also use null for * field, but not recommended that you do so. * * If $value does not exist, the return value will be false. If $taxonomy exists * and $field and $value combinations exist, the term will be returned. * * This function will always return the first term that matches the `$field`- * `$value`-`$taxonomy` combination specified in the parameters. If your query * is likely to match more than one term (as is likely to be the case when * `$field` is 'name', for example), consider using get_terms() instead; that * way, you will get all matching terms, and can provide your own logic for * deciding which one was intended. * * @todo Better formatting for DocBlock. * * @since 2.3.0 * @since 4.4.0 `$taxonomy` is optional if `$field` is 'term_taxonomy_id'. Converted to return * a WP_Term object if `$output` is `OBJECT`. * @since 5.5.0 Added 'ID' as an alias of 'id' for the `$field` parameter. * * @see sanitize_term_field() The $context param lists the available values for get_term_by() $filter param. * * @param string $field Either 'slug', 'name', 'term_id' (or 'id', 'ID'), or 'term_taxonomy_id'. * @param string|int $value Search for this term value. * @param string $taxonomy Taxonomy name. Optional, if `$field` is 'term_taxonomy_id'. * @param string $output Optional. The required return type. One of OBJECT, ARRAY_A, or ARRAY_N, which * correspond to a WP_Term object, an associative array, or a numeric array, * respectively. Default OBJECT. * @param string $filter Optional. How to sanitize term fields. Default 'raw'. * @return WP_Term|array|false WP_Term instance (or array) on success, depending on the `$output` value. * False if `$taxonomy` does not exist or `$term` was not found. */ function get_term_by( $field, $value, $taxonomy = '', $output = OBJECT, $filter = 'raw' ) { // 'term_taxonomy_id' lookups don't require taxonomy checks. if ( 'term_taxonomy_id' !== $field && ! taxonomy_exists( $taxonomy ) ) { return false; } // No need to perform a query for empty 'slug' or 'name'. if ( 'slug' === $field || 'name' === $field ) { $value = (string) $value; if ( 0 === strlen( $value ) ) { return false; } } if ( 'id' === $field || 'ID' === $field || 'term_id' === $field ) { $term = get_term( (int) $value, $taxonomy, $output, $filter ); if ( is_wp_error( $term ) || null === $term ) { $term = false; } return $term; } $args = array( 'get' => 'all', 'number' => 1, 'taxonomy' => $taxonomy, 'update_term_meta_cache' => false, 'orderby' => 'none', 'suppress_filter' => true, ); switch ( $field ) { case 'slug': $args['slug'] = $value; break; case 'name': $args['name'] = $value; break; case 'term_taxonomy_id': $args['term_taxonomy_id'] = $value; unset( $args['taxonomy'] ); break; default: return false; } $terms = get_terms( $args ); if ( is_wp_error( $terms ) || empty( $terms ) ) { return false; } $term = array_shift( $terms ); // In the case of 'term_taxonomy_id', override the provided `$taxonomy` with whatever we find in the DB. if ( 'term_taxonomy_id' === $field ) { $taxonomy = $term->taxonomy; } return get_term( $term, $taxonomy, $output, $filter ); } /** * Merges all term children into a single array of their IDs. * * This recursive function will merge all of the children of $term into the same * array of term IDs. Only useful for taxonomies which are hierarchical. * * Will return an empty array if $term does not exist in $taxonomy. * * @since 2.3.0 * * @param int $term_id ID of term to get children. * @param string $taxonomy Taxonomy name. * @return array|WP_Error List of term IDs. WP_Error returned if `$taxonomy` does not exist. */ function get_term_children( $term_id, $taxonomy ) { if ( ! taxonomy_exists( $taxonomy ) ) { return new WP_Error( 'invalid_taxonomy', __( 'Invalid taxonomy.' ) ); } $term_id = (int) $term_id; $terms = _get_term_hierarchy( $taxonomy ); if ( ! isset( $terms[ $term_id ] ) ) { return array(); } $children = $terms[ $term_id ]; foreach ( (array) $terms[ $term_id ] as $child ) { if ( $term_id === $child ) { continue; } if ( isset( $terms[ $child ] ) ) { $children = array_merge( $children, get_term_children( $child, $taxonomy ) ); } } return $children; } /** * Gets sanitized term field. * * The function is for contextual reasons and for simplicity of usage. * * @since 2.3.0 * @since 4.4.0 The `$taxonomy` parameter was made optional. `$term` can also now accept a WP_Term object. * * @see sanitize_term_field() * * @param string $field Term field to fetch. * @param int|WP_Term $term Term ID or object. * @param string $taxonomy Optional. Taxonomy name. Default empty. * @param string $context Optional. How to sanitize term fields. Look at sanitize_term_field() for available options. * Default 'display'. * @return string|int|null|WP_Error Will return an empty string if $term is not an object or if $field is not set in $term. */ function get_term_field( $field, $term, $taxonomy = '', $context = 'display' ) { $term = get_term( $term, $taxonomy ); if ( is_wp_error( $term ) ) { return $term; } if ( ! is_object( $term ) ) { return ''; } if ( ! isset( $term->$field ) ) { return ''; } return sanitize_term_field( $field, $term->$field, $term->term_id, $term->taxonomy, $context ); } /** * Sanitizes term for editing. * * Return value is sanitize_term() and usage is for sanitizing the term for * editing. Function is for contextual and simplicity. * * @since 2.3.0 * * @param int|object $id Term ID or object. * @param string $taxonomy Taxonomy name. * @return string|int|null|WP_Error Will return empty string if $term is not an object. */ function get_term_to_edit( $id, $taxonomy ) { $term = get_term( $id, $taxonomy ); if ( is_wp_error( $term ) ) { return $term; } if ( ! is_object( $term ) ) { return ''; } return sanitize_term( $term, $taxonomy, 'edit' ); } /** * Retrieves the terms in a given taxonomy or list of taxonomies. * * You can fully inject any customizations to the query before it is sent, as * well as control the output with a filter. * * The return type varies depending on the value passed to `$args['fields']`. See * WP_Term_Query::get_terms() for details. In all cases, a `WP_Error` object will * be returned if an invalid taxonomy is requested. * * The {@see 'get_terms'} filter will be called when the cache has the term and will * pass the found term along with the array of $taxonomies and array of $args. * This filter is also called before the array of terms is passed and will pass * the array of terms, along with the $taxonomies and $args. * * The {@see 'list_terms_exclusions'} filter passes the compiled exclusions along with * the $args. * * The {@see 'get_terms_orderby'} filter passes the `ORDER BY` clause for the query * along with the $args array. * * Taxonomy or an array of taxonomies should be passed via the 'taxonomy' argument * in the `$args` array: * * $terms = get_terms( array( * 'taxonomy' => 'post_tag', * 'hide_empty' => false, * ) ); * * Prior to 4.5.0, taxonomy was passed as the first parameter of `get_terms()`. * * @since 2.3.0 * @since 4.2.0 Introduced 'name' and 'childless' parameters. * @since 4.4.0 Introduced the ability to pass 'term_id' as an alias of 'id' for the `orderby` parameter. * Introduced the 'meta_query' and 'update_term_meta_cache' parameters. Converted to return * a list of WP_Term objects. * @since 4.5.0 Changed the function signature so that the `$args` array can be provided as the first parameter. * Introduced 'meta_key' and 'meta_value' parameters. Introduced the ability to order results by metadata. * @since 4.8.0 Introduced 'suppress_filter' parameter. * * @internal The `$deprecated` parameter is parsed for backward compatibility only. * * @param array|string $args Optional. Array or string of arguments. See WP_Term_Query::__construct() * for information on accepted arguments. Default empty array. * @param array|string $deprecated Optional. Argument array, when using the legacy function parameter format. * If present, this parameter will be interpreted as `$args`, and the first * function parameter will be parsed as a taxonomy or array of taxonomies. * Default empty. * @return WP_Term[]|int[]|string[]|string|WP_Error Array of terms, a count thereof as a numeric string, * or WP_Error if any of the taxonomies do not exist. * See the function description for more information. */ function get_terms( $args = array(), $deprecated = '' ) { $term_query = new WP_Term_Query(); $defaults = array( 'suppress_filter' => false, ); /* * Legacy argument format ($taxonomy, $args) takes precedence. * * We detect legacy argument format by checking if * (a) a second non-empty parameter is passed, or * (b) the first parameter shares no keys with the default array (ie, it's a list of taxonomies) */ $_args = wp_parse_args( $args ); $key_intersect = array_intersect_key( $term_query->query_var_defaults, (array) $_args ); $do_legacy_args = $deprecated || empty( $key_intersect ); if ( $do_legacy_args ) { $taxonomies = (array) $args; $args = wp_parse_args( $deprecated, $defaults ); $args['taxonomy'] = $taxonomies; } else { $args = wp_parse_args( $args, $defaults ); if ( isset( $args['taxonomy'] ) && null !== $args['taxonomy'] ) { $args['taxonomy'] = (array) $args['taxonomy']; } } if ( ! empty( $args['taxonomy'] ) ) { foreach ( $args['taxonomy'] as $taxonomy ) { if ( ! taxonomy_exists( $taxonomy ) ) { return new WP_Error( 'invalid_taxonomy', __( 'Invalid taxonomy.' ) ); } } } // Don't pass suppress_filter to WP_Term_Query. $suppress_filter = $args['suppress_filter']; unset( $args['suppress_filter'] ); $terms = $term_query->query( $args ); // Count queries are not filtered, for legacy reasons. if ( ! is_array( $terms ) ) { return $terms; } if ( $suppress_filter ) { return $terms; } /** * Filters the found terms. * * @since 2.3.0 * @since 4.6.0 Added the `$term_query` parameter. * * @param array $terms Array of found terms. * @param array|null $taxonomies An array of taxonomies if known. * @param array $args An array of get_terms() arguments. * @param WP_Term_Query $term_query The WP_Term_Query object. */ return apply_filters( 'get_terms', $terms, $term_query->query_vars['taxonomy'], $term_query->query_vars, $term_query ); } /** * Adds metadata to a term. * * @since 4.4.0 * * @param int $term_id Term 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|WP_Error Meta ID on success, false on failure. * WP_Error when term_id is ambiguous between taxonomies. */ function add_term_meta( $term_id, $meta_key, $meta_value, $unique = false ) { if ( wp_term_is_shared( $term_id ) ) { return new WP_Error( 'ambiguous_term_id', __( 'Term meta cannot be added to terms that are shared between taxonomies.' ), $term_id ); } return add_metadata( 'term', $term_id, $meta_key, $meta_value, $unique ); } /** * Removes metadata matching criteria from a term. * * @since 4.4.0 * * @param int $term_id Term ID. * @param string $meta_key Metadata name. * @param mixed $meta_value Optional. Metadata value. If provided, * rows will only be removed that match the value. * Must be serializable if non-scalar. Default empty. * @return bool True on success, false on failure. */ function delete_term_meta( $term_id, $meta_key, $meta_value = '' ) { return delete_metadata( 'term', $term_id, $meta_key, $meta_value ); } /** * Retrieves metadata for a term. * * @since 4.4.0 * * @param int $term_id Term 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. * This parameter has no effect if `$key` is not specified. * Default false. * @return mixed An array of values if `$single` is false. * The value of the meta field if `$single` is true. * False for an invalid `$term_id` (non-numeric, zero, or negative value). * An empty string if a valid but non-existing term ID is passed. */ function get_term_meta( $term_id, $key = '', $single = false ) { return get_metadata( 'term', $term_id, $key, $single ); } /** * Updates term metadata. * * Use the `$prev_value` parameter to differentiate between meta fields with the same key and term ID. * * If the meta field for the term does not exist, it will be added. * * @since 4.4.0 * * @param int $term_id Term 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 updating. * If specified, only update existing metadata entries with * this value. Otherwise, update all entries. Default empty. * @return int|bool|WP_Error Meta ID if the key didn't exist. true on successful update, * false on failure or if the value passed to the function * is the same as the one that is already in the database. * WP_Error when term_id is ambiguous between taxonomies. */ function update_term_meta( $term_id, $meta_key, $meta_value, $prev_value = '' ) { if ( wp_term_is_shared( $term_id ) ) { return new WP_Error( 'ambiguous_term_id', __( 'Term meta cannot be added to terms that are shared between taxonomies.' ), $term_id ); } return update_metadata( 'term', $term_id, $meta_key, $meta_value, $prev_value ); } /** * Updates metadata cache for list of term IDs. * * Performs SQL query to retrieve all metadata for the terms matching `$term_ids` and stores them in the cache. * Subsequent calls to `get_term_meta()` will not need to query the database. * * @since 4.4.0 * * @param array $term_ids List of term IDs. * @return array|false An array of metadata on success, false if there is nothing to update. */ function update_termmeta_cache( $term_ids ) { return update_meta_cache( 'term', $term_ids ); } /** * Queue term meta for lazy-loading. * * @since 6.3.0 * * @param array $term_ids List of term IDs. */ function wp_lazyload_term_meta( array $term_ids ) { if ( empty( $term_ids ) ) { return; } $lazyloader = wp_metadata_lazyloader(); $lazyloader->queue_objects( 'term', $term_ids ); } /** * Gets all meta data, including meta IDs, for the given term ID. * * @since 4.9.0 * * @global wpdb $wpdb WordPress database abstraction object. * * @param int $term_id Term ID. * @return array|false Array with meta data, or false when the meta table is not installed. */ function has_term_meta( $term_id ) { $check = wp_check_term_meta_support_prefilter( null ); if ( null !== $check ) { return $check; } global $wpdb; return $wpdb->get_results( $wpdb->prepare( "SELECT meta_key, meta_value, meta_id, term_id FROM $wpdb->termmeta WHERE term_id = %d ORDER BY meta_key,meta_id", $term_id ), ARRAY_A ); } /** * Registers a meta key for terms. * * @since 4.9.8 * * @param string $taxonomy Taxonomy to register a meta key for. Pass an empty string * to register the meta key across all existing taxonomies. * @param string $meta_key The meta key to register. * @param array $args Data used to describe the meta key when registered. See * {@see register_meta()} for a list of supported arguments. * @return bool True if the meta key was successfully registered, false if not. */ function register_term_meta( $taxonomy, $meta_key, array $args ) { $args['object_subtype'] = $taxonomy; return register_meta( 'term', $meta_key, $args ); } /** * Unregisters a meta key for terms. * * @since 4.9.8 * * @param string $taxonomy Taxonomy the meta key is currently registered for. Pass * an empty string if the meta key is registered across all * existing taxonomies. * @param string $meta_key The meta key to unregister. * @return bool True on success, false if the meta key was not previously registered. */ function unregister_term_meta( $taxonomy, $meta_key ) { return unregister_meta_key( 'term', $meta_key, $taxonomy ); } /** * Determines whether a taxonomy term exists. * * Formerly is_term(), introduced in 2.3.0. * * For more information on this and similar theme functions, check out * the {@link https://developer.wordpress.org/themes/basics/conditional-tags/ * Conditional Tags} article in the Theme Developer Handbook. * * @since 3.0.0 * @since 6.0.0 Converted to use `get_terms()`. * * @global bool $_wp_suspend_cache_invalidation * * @param int|string $term The term to check. Accepts term ID, slug, or name. * @param string $taxonomy Optional. The taxonomy name to use. * @param int $parent_term Optional. ID of parent term under which to confine the exists search. * @return mixed Returns null if the term does not exist. * Returns the term ID if no taxonomy is specified and the term ID exists. * Returns an array of the term ID and the term taxonomy ID if the taxonomy is specified and the pairing exists. * Returns 0 if term ID 0 is passed to the function. */ function term_exists( $term, $taxonomy = '', $parent_term = null ) { global $_wp_suspend_cache_invalidation; if ( null === $term ) { return null; } $defaults = array( 'get' => 'all', 'fields' => 'ids', 'number' => 1, 'update_term_meta_cache' => false, 'order' => 'ASC', 'orderby' => 'term_id', 'suppress_filter' => true, ); // Ensure that while importing, queries are not cached. if ( ! empty( $_wp_suspend_cache_invalidation ) ) { $defaults['cache_results'] = false; } if ( ! empty( $taxonomy ) ) { $defaults['taxonomy'] = $taxonomy; $defaults['fields'] = 'all'; } /** * Filters default query arguments for checking if a term exists. * * @since 6.0.0 * * @param array $defaults An array of arguments passed to get_terms(). * @param int|string $term The term to check. Accepts term ID, slug, or name. * @param string $taxonomy The taxonomy name to use. An empty string indicates * the search is against all taxonomies. * @param int|null $parent_term ID of parent term under which to confine the exists search. * Null indicates the search is unconfined. */ $defaults = apply_filters( 'term_exists_default_query_args', $defaults, $term, $taxonomy, $parent_term ); if ( is_int( $term ) ) { if ( 0 === $term ) { return 0; } $args = wp_parse_args( array( 'include' => array( $term ) ), $defaults ); $terms = get_terms( $args ); } else { $term = trim( wp_unslash( $term ) ); if ( '' === $term ) { return null; } if ( ! empty( $taxonomy ) && is_numeric( $parent_term ) ) { $defaults['parent'] = (int) $parent_term; } $args = wp_parse_args( array( 'slug' => sanitize_title( $term ) ), $defaults ); $terms = get_terms( $args ); if ( empty( $terms ) || is_wp_error( $terms ) ) { $args = wp_parse_args( array( 'name' => $term ), $defaults ); $terms = get_terms( $args ); } } if ( empty( $terms ) || is_wp_error( $terms ) ) { return null; } $_term = array_shift( $terms ); if ( ! empty( $taxonomy ) ) { return array( 'term_id' => (string) $_term->term_id, 'term_taxonomy_id' => (string) $_term->term_taxonomy_id, ); } return (string) $_term; } /** * Checks if a term is an ancestor of another term. * * You can use either an ID or the term object for both parameters. * * @since 3.4.0 * * @param int|object $term1 ID or object to check if this is the parent term. * @param int|object $term2 The child term. * @param string $taxonomy Taxonomy name that $term1 and `$term2` belong to. * @return bool Whether `$term2` is a child of `$term1`. */ function term_is_ancestor_of( $term1, $term2, $taxonomy ) { if ( ! isset( $term1->term_id ) ) { $term1 = get_term( $term1, $taxonomy ); } if ( ! isset( $term2->parent ) ) { $term2 = get_term( $term2, $taxonomy ); } if ( empty( $term1->term_id ) || empty( $term2->parent ) ) { return false; } if ( $term2->parent === $term1->term_id ) { return true; } return term_is_ancestor_of( $term1, get_term( $term2->parent, $taxonomy ), $taxonomy ); } /** * Sanitizes all term fields. * * Relies on sanitize_term_field() to sanitize the term. The difference is that * this function will sanitize **all** fields. The context is based * on sanitize_term_field(). * * The `$term` is expected to be either an array or an object. * * @since 2.3.0 * * @param array|object $term The term to check. * @param string $taxonomy The taxonomy name to use. * @param string $context Optional. Context in which to sanitize the term. * Accepts 'raw', 'edit', 'db', 'display', 'rss', * 'attribute', or 'js'. Default 'display'. * @return array|object Term with all fields sanitized. */ function sanitize_term( $term, $taxonomy, $context = 'display' ) { $fields = array( 'term_id', 'name', 'description', 'slug', 'count', 'parent', 'term_group', 'term_taxonomy_id', 'object_id' ); $do_object = is_object( $term ); $term_id = $do_object ? $term->term_id : ( isset( $term['term_id'] ) ? $term['term_id'] : 0 ); foreach ( (array) $fields as $field ) { if ( $do_object ) { if ( isset( $term->$field ) ) { $term->$field = sanitize_term_field( $field, $term->$field, $term_id, $taxonomy, $context ); } } else { if ( isset( $term[ $field ] ) ) { $term[ $field ] = sanitize_term_field( $field, $term[ $field ], $term_id, $taxonomy, $context ); } } } if ( $do_object ) { $term->filter = $context; } else { $term['filter'] = $context; } return $term; } /** * Sanitizes the field value in the term based on the context. * * Passing a term field value through the function should be assumed to have * cleansed the value for whatever context the term field is going to be used. * * If no context or an unsupported context is given, then default filters will * be applied. * * There are enough filters for each context to support a custom filtering * without creating your own filter function. Simply create a function that * hooks into the filter you need. * * @since 2.3.0 * * @param string $field Term field to sanitize. * @param string $value Search for this term value. * @param int $term_id Term ID. * @param string $taxonomy Taxonomy name. * @param string $context Context in which to sanitize the term field. * Accepts 'raw', 'edit', 'db', 'display', 'rss', * 'attribute', or 'js'. Default 'display'. * @return mixed Sanitized field. */ function sanitize_term_field( $field, $value, $term_id, $taxonomy, $context ) { $int_fields = array( 'parent', 'term_id', 'count', 'term_group', 'term_taxonomy_id', 'object_id' ); if ( in_array( $field, $int_fields, true ) ) { $value = (int) $value; if ( $value < 0 ) { $value = 0; } } $context = strtolower( $context ); if ( 'raw' === $context ) { return $value; } if ( 'edit' === $context ) { /** * Filters a term field to edit before it is sanitized. * * The dynamic portion of the hook name, `$field`, refers to the term field. * * @since 2.3.0 * * @param mixed $value Value of the term field. * @param int $term_id Term ID. * @param string $taxonomy Taxonomy slug. */ $value = apply_filters( "edit_term_{$field}", $value, $term_id, $taxonomy ); /** * Filters the taxonomy field to edit before it is sanitized. * * The dynamic portions of the filter name, `$taxonomy` and `$field`, refer * to the taxonomy slug and taxonomy field, respectively. * * @since 2.3.0 * * @param mixed $value Value of the taxonomy field to edit. * @param int $term_id Term ID. */ $value = apply_filters( "edit_{$taxonomy}_{$field}", $value, $term_id ); if ( 'description' === $field ) { $value = esc_html( $value ); // textarea_escaped } else { $value = esc_attr( $value ); } } elseif ( 'db' === $context ) { /** * Filters a term field value before it is sanitized. * * The dynamic portion of the hook name, `$field`, refers to the term field. * * @since 2.3.0 * * @param mixed $value Value of the term field. * @param string $taxonomy Taxonomy slug. */ $value = apply_filters( "pre_term_{$field}", $value, $taxonomy ); /** * Filters a taxonomy field before it is sanitized. * * The dynamic portions of the filter name, `$taxonomy` and `$field`, refer * to the taxonomy slug and field name, respectively. * * @since 2.3.0 * * @param mixed $value Value of the taxonomy field. */ $value = apply_filters( "pre_{$taxonomy}_{$field}", $value ); // Back compat filters. if ( 'slug' === $field ) { /** * Filters the category nicename before it is sanitized. * * Use the {@see 'pre_$taxonomy_$field'} hook instead. * * @since 2.0.3 * * @param string $value The category nicename. */ $value = apply_filters( 'pre_category_nicename', $value ); } } elseif ( 'rss' === $context ) { /** * Filters the term field for use in RSS. * * The dynamic portion of the hook name, `$field`, refers to the term field. * * @since 2.3.0 * * @param mixed $value Value of the term field. * @param string $taxonomy Taxonomy slug. */ $value = apply_filters( "term_{$field}_rss", $value, $taxonomy ); /** * Filters the taxonomy field for use in RSS. * * The dynamic portions of the hook name, `$taxonomy`, and `$field`, refer * to the taxonomy slug and field name, respectively. * * @since 2.3.0 * * @param mixed $value Value of the taxonomy field. */ $value = apply_filters( "{$taxonomy}_{$field}_rss", $value ); } else { // Use display filters by default. /** * Filters the term field sanitized for display. * * The dynamic portion of the hook name, `$field`, refers to the term field name. * * @since 2.3.0 * * @param mixed $value Value of the term field. * @param int $term_id Term ID. * @param string $taxonomy Taxonomy slug. * @param string $context Context to retrieve the term field value. */ $value = apply_filters( "term_{$field}", $value, $term_id, $taxonomy, $context ); /** * Filters the taxonomy field sanitized for display. * * The dynamic portions of the filter name, `$taxonomy`, and `$field`, refer * to the taxonomy slug and taxonomy field, respectively. * * @since 2.3.0 * * @param mixed $value Value of the taxonomy field. * @param int $term_id Term ID. * @param string $context Context to retrieve the taxonomy field value. */ $value = apply_filters( "{$taxonomy}_{$field}", $value, $term_id, $context ); } if ( 'attribute' === $context ) { $value = esc_attr( $value ); } elseif ( 'js' === $context ) { $value = esc_js( $value ); } // Restore the type for integer fields after esc_attr(). if ( in_array( $field, $int_fields, true ) ) { $value = (int) $value; } return $value; } /** * Counts how many terms are in taxonomy. * * Default $args is 'hide_empty' which can be 'hide_empty=true' or array('hide_empty' => true). * * @since 2.3.0 * @since 5.6.0 Changed the function signature so that the `$args` array can be provided as the first parameter. * * @internal The `$deprecated` parameter is parsed for backward compatibility only. * * @param array|string $args Optional. Array or string of arguments. See WP_Term_Query::__construct() * for information on accepted arguments. Default empty array. * @param array|string $deprecated Optional. Argument array, when using the legacy function parameter format. * If present, this parameter will be interpreted as `$args`, and the first * function parameter will be parsed as a taxonomy or array of taxonomies. * Default empty. * @return string|WP_Error Numeric string containing the number of terms in that * taxonomy or WP_Error if the taxonomy does not exist. */ function wp_count_terms( $args = array(), $deprecated = '' ) { $use_legacy_args = false; // Check whether function is used with legacy signature: `$taxonomy` and `$args`. if ( $args && ( is_string( $args ) && taxonomy_exists( $args ) || is_array( $args ) && wp_is_numeric_array( $args ) ) ) { $use_legacy_args = true; } $defaults = array( 'hide_empty' => false ); if ( $use_legacy_args ) { $defaults['taxonomy'] = $args; $args = $deprecated; } $args = wp_parse_args( $args, $defaults ); // Backward compatibility. if ( isset( $args['ignore_empty'] ) ) { $args['hide_empty'] = $args['ignore_empty']; unset( $args['ignore_empty'] ); } $args['fields'] = 'count'; return get_terms( $args ); } /** * Unlinks the object from the taxonomy or taxonomies. * * Will remove all relationships between the object and any terms in * a particular taxonomy or taxonomies. Does not remove the term or * taxonomy itself. * * @since 2.3.0 * * @param int $object_id The term object ID that refers to the term. * @param string|array $taxonomies List of taxonomy names or single taxonomy name. */ function wp_delete_object_term_relationships( $object_id, $taxonomies ) { $object_id = (int) $object_id; if ( ! is_array( $taxonomies ) ) { $taxonomies = array( $taxonomies ); } foreach ( (array) $taxonomies as $taxonomy ) { $term_ids = wp_get_object_terms( $object_id, $taxonomy, array( 'fields' => 'ids' ) ); $term_ids = array_map( 'intval', $term_ids ); wp_remove_object_terms( $object_id, $term_ids, $taxonomy ); } } /** * Removes a term from the database. * * If the term is a parent of other terms, then the children will be updated to * that term's parent. * * Metadata associated with the term will be deleted. * * @since 2.3.0 * * @global wpdb $wpdb WordPress database abstraction object. * * @param int $term Term ID. * @param string $taxonomy Taxonomy name. * @param array|string $args { * Optional. Array of arguments to override the default term ID. Default empty array. * * @type int $default The term ID to make the default term. This will only override * the terms found if there is only one term found. Any other and * the found terms are used. * @type bool $force_default Optional. Whether to force the supplied term as default to be * assigned even if the object was not going to be term-less. * Default false. * } * @return bool|int|WP_Error True on success, false if term does not exist. Zero on attempted * deletion of default Category. WP_Error if the taxonomy does not exist. */ function wp_delete_term( $term, $taxonomy, $args = array() ) { global $wpdb; $term = (int) $term; $ids = term_exists( $term, $taxonomy ); if ( ! $ids ) { return false; } if ( is_wp_error( $ids ) ) { return $ids; } $tt_id = $ids['term_taxonomy_id']; $defaults = array(); if ( 'category' === $taxonomy ) { $defaults['default'] = (int) get_option( 'default_category' ); if ( $defaults['default'] === $term ) { return 0; // Don't delete the default category. } } // Don't delete the default custom taxonomy term. $taxonomy_object = get_taxonomy( $taxonomy ); if ( ! empty( $taxonomy_object->default_term ) ) { $defaults['default'] = (int) get_option( 'default_term_' . $taxonomy ); if ( $defaults['default'] === $term ) { return 0; } } $args = wp_parse_args( $args, $defaults ); if ( isset( $args['default'] ) ) { $default = (int) $args['default']; if ( ! term_exists( $default, $taxonomy ) ) { unset( $default ); } } if ( isset( $args['force_default'] ) ) { $force_default = $args['force_default']; } /** * Fires when deleting a term, before any modifications are made to posts or terms. * * @since 4.1.0 * * @param int $term Term ID. * @param string $taxonomy Taxonomy name. */ do_action( 'pre_delete_term', $term, $taxonomy ); // Update children to point to new parent. if ( is_taxonomy_hierarchical( $taxonomy ) ) { $term_obj = get_term( $term, $taxonomy ); if ( is_wp_error( $term_obj ) ) { return $term_obj; } $parent = $term_obj->parent; $edit_ids = $wpdb->get_results( "SELECT term_id, term_taxonomy_id FROM $wpdb->term_taxonomy WHERE `parent` = " . (int) $term_obj->term_id ); $edit_tt_ids = wp_list_pluck( $edit_ids, 'term_taxonomy_id' ); /** * Fires immediately before a term to delete's children are reassigned a parent. * * @since 2.9.0 * * @param array $edit_tt_ids An array of term taxonomy IDs for the given term. */ do_action( 'edit_term_taxonomies', $edit_tt_ids ); $wpdb->update( $wpdb->term_taxonomy, compact( 'parent' ), array( 'parent' => $term_obj->term_id ) + compact( 'taxonomy' ) ); // Clean the cache for all child terms. $edit_term_ids = wp_list_pluck( $edit_ids, 'term_id' ); clean_term_cache( $edit_term_ids, $taxonomy ); /** * Fires immediately after a term to delete's children are reassigned a parent. * * @since 2.9.0 * * @param array $edit_tt_ids An array of term taxonomy IDs for the given term. */ do_action( 'edited_term_taxonomies', $edit_tt_ids ); } // Get the term before deleting it or its term relationships so we can pass to actions below. $deleted_term = get_term( $term, $taxonomy ); $object_ids = (array) $wpdb->get_col( $wpdb->prepare( "SELECT object_id FROM $wpdb->term_relationships WHERE term_taxonomy_id = %d", $tt_id ) ); foreach ( $object_ids as $object_id ) { if ( ! isset( $default ) ) { wp_remove_object_terms( $object_id, $term, $taxonomy ); continue; } $terms = wp_get_object_terms( $object_id, $taxonomy, array( 'fields' => 'ids', 'orderby' => 'none', ) ); if ( 1 === count( $terms ) && isset( $default ) ) { $terms = array( $default ); } else { $terms = array_diff( $terms, array( $term ) ); if ( isset( $default ) && isset( $force_default ) && $force_default ) { $terms = array_merge( $terms, array( $default ) ); } } $terms = array_map( 'intval', $terms ); wp_set_object_terms( $object_id, $terms, $taxonomy ); } // Clean the relationship caches for all object types using this term. $tax_object = get_taxonomy( $taxonomy ); foreach ( $tax_object->object_type as $object_type ) { clean_object_term_cache( $object_ids, $object_type ); } $term_meta_ids = $wpdb->get_col( $wpdb->prepare( "SELECT meta_id FROM $wpdb->termmeta WHERE term_id = %d ", $term ) ); foreach ( $term_meta_ids as $mid ) { delete_metadata_by_mid( 'term', $mid ); } /** * Fires immediately before a term taxonomy ID is deleted. * * @since 2.9.0 * * @param int $tt_id Term taxonomy ID. */ do_action( 'delete_term_taxonomy', $tt_id ); $wpdb->delete( $wpdb->term_taxonomy, array( 'term_taxonomy_id' => $tt_id ) ); /** * Fires immediately after a term taxonomy ID is deleted. * * @since 2.9.0 * * @param int $tt_id Term taxonomy ID. */ do_action( 'deleted_term_taxonomy', $tt_id ); // Delete the term if no taxonomies use it. if ( ! $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM $wpdb->term_taxonomy WHERE term_id = %d", $term ) ) ) { $wpdb->delete( $wpdb->terms, array( 'term_id' => $term ) ); } clean_term_cache( $term, $taxonomy ); /** * Fires after a term is deleted from the database and the cache is cleaned. * * The {@see 'delete_$taxonomy'} hook is also available for targeting a specific * taxonomy. * * @since 2.5.0 * @since 4.5.0 Introduced the `$object_ids` argument. * * @param int $term Term ID. * @param int $tt_id Term taxonomy ID. * @param string $taxonomy Taxonomy slug. * @param WP_Term $deleted_term Copy of the already-deleted term. * @param array $object_ids List of term object IDs. */ do_action( 'delete_term', $term, $tt_id, $taxonomy, $deleted_term, $object_ids ); /** * Fires after a term in a specific taxonomy is deleted. * * The dynamic portion of the hook name, `$taxonomy`, refers to the specific * taxonomy the term belonged to. * * Possible hook names include: * * - `delete_category` * - `delete_post_tag` * * @since 2.3.0 * @since 4.5.0 Introduced the `$object_ids` argument. * * @param int $term Term ID. * @param int $tt_id Term taxonomy ID. * @param WP_Term $deleted_term Copy of the already-deleted term. * @param array $object_ids List of term object IDs. */ do_action( "delete_{$taxonomy}", $term, $tt_id, $deleted_term, $object_ids ); return true; } /** * Deletes one existing category. * * @since 2.0.0 * * @param int $cat_id Category term ID. * @return bool|int|WP_Error Returns true if completes delete action; false if term doesn't exist; * Zero on attempted deletion of default Category; WP_Error object is * also a possibility. */ function wp_delete_category( $cat_id ) { return wp_delete_term( $cat_id, 'category' ); } /** * Retrieves the terms associated with the given object(s), in the supplied taxonomies. * * @since 2.3.0 * @since 4.2.0 Added support for 'taxonomy', 'parent', and 'term_taxonomy_id' values of `$orderby`. * Introduced `$parent` argument. * @since 4.4.0 Introduced `$meta_query` and `$update_term_meta_cache` arguments. When `$fields` is 'all' or * 'all_with_object_id', an array of `WP_Term` objects will be returned. * @since 4.7.0 Refactored to use WP_Term_Query, and to support any WP_Term_Query arguments. * @since 6.3.0 Passing `update_term_meta_cache` argument value false by default resulting in get_terms() to not * prime the term meta cache. * * @param int|int[] $object_ids The ID(s) of the object(s) to retrieve. * @param string|string[] $taxonomies The taxonomy names to retrieve terms from. * @param array|string $args See WP_Term_Query::__construct() for supported arguments. * @return WP_Term[]|int[]|string[]|string|WP_Error Array of terms, a count thereof as a numeric string, * or WP_Error if any of the taxonomies do not exist. * See WP_Term_Query::get_terms() for more information. */ function wp_get_object_terms( $object_ids, $taxonomies, $args = array() ) { if ( empty( $object_ids ) || empty( $taxonomies ) ) { return array(); } if ( ! is_array( $taxonomies ) ) { $taxonomies = array( $taxonomies ); } foreach ( $taxonomies as $taxonomy ) { if ( ! taxonomy_exists( $taxonomy ) ) { return new WP_Error( 'invalid_taxonomy', __( 'Invalid taxonomy.' ) ); } } if ( ! is_array( $object_ids ) ) { $object_ids = array( $object_ids ); } $object_ids = array_map( 'intval', $object_ids ); $defaults = array( 'update_term_meta_cache' => false, ); $args = wp_parse_args( $args, $defaults ); /** * Filters arguments for retrieving object terms. * * @since 4.9.0 * * @param array $args An array of arguments for retrieving terms for the given object(s). * See {@see wp_get_object_terms()} for details. * @param int[] $object_ids Array of object IDs. * @param string[] $taxonomies Array of taxonomy names to retrieve terms from. */ $args = apply_filters( 'wp_get_object_terms_args', $args, $object_ids, $taxonomies ); /* * When one or more queried taxonomies is registered with an 'args' array, * those params override the `$args` passed to this function. */ $terms = array(); if ( count( $taxonomies ) > 1 ) { foreach ( $taxonomies as $index => $taxonomy ) { $t = get_taxonomy( $taxonomy ); if ( isset( $t->args ) && is_array( $t->args ) && array_merge( $args, $t->args ) != $args ) { unset( $taxonomies[ $index ] ); $terms = array_merge( $terms, wp_get_object_terms( $object_ids, $taxonomy, array_merge( $args, $t->args ) ) ); } } } else { $t = get_taxonomy( $taxonomies[0] ); if ( isset( $t->args ) && is_array( $t->args ) ) { $args = array_merge( $args, $t->args ); } } $args['taxonomy'] = $taxonomies; $args['object_ids'] = $object_ids; // Taxonomies registered without an 'args' param are handled here. if ( ! empty( $taxonomies ) ) { $terms_from_remaining_taxonomies = get_terms( $args ); // Array keys should be preserved for values of $fields that use term_id for keys. if ( ! empty( $args['fields'] ) && str_starts_with( $args['fields'], 'id=>' ) ) { $terms = $terms + $terms_from_remaining_taxonomies; } else { $terms = array_merge( $terms, $terms_from_remaining_taxonomies ); } } /** * Filters the terms for a given object or objects. * * @since 4.2.0 * * @param WP_Term[]|int[]|string[]|string $terms Array of terms or a count thereof as a numeric string. * @param int[] $object_ids Array of object IDs for which terms were retrieved. * @param string[] $taxonomies Array of taxonomy names from which terms were retrieved. * @param array $args Array of arguments for retrieving terms for the given * object(s). See wp_get_object_terms() for details. */ $terms = apply_filters( 'get_object_terms', $terms, $object_ids, $taxonomies, $args ); $object_ids = implode( ',', $object_ids ); $taxonomies = "'" . implode( "', '", array_map( 'esc_sql', $taxonomies ) ) . "'"; /** * Filters the terms for a given object or objects. * * The `$taxonomies` parameter passed to this filter is formatted as a SQL fragment. The * {@see 'get_object_terms'} filter is recommended as an alternative. * * @since 2.8.0 * * @param WP_Term[]|int[]|string[]|string $terms Array of terms or a count thereof as a numeric string. * @param string $object_ids Comma separated list of object IDs for which terms were retrieved. * @param string $taxonomies SQL fragment of taxonomy names from which terms were retrieved. * @param array $args Array of arguments for retrieving terms for the given * object(s). See wp_get_object_terms() for details. */ return apply_filters( 'wp_get_object_terms', $terms, $object_ids, $taxonomies, $args ); } /** * Adds a new term to the database. * * A non-existent term is inserted in the following sequence: * 1. The term is added to the term table, then related to the taxonomy. * 2. If everything is correct, several actions are fired. * 3. The 'term_id_filter' is evaluated. * 4. The term cache is cleaned. * 5. Several more actions are fired. * 6. An array is returned containing the `term_id` and `term_taxonomy_id`. * * If the 'slug' argument is not empty, then it is checked to see if the term * is invalid. If it is not a valid, existing term, it is added and the term_id * is given. * * If the taxonomy is hierarchical, and the 'parent' argument is not empty, * the term is inserted and the term_id will be given. * * Error handling: * If `$taxonomy` does not exist or `$term` is empty, * a WP_Error object will be returned. * * If the term already exists on the same hierarchical level, * or the term slug and name are not unique, a WP_Error object will be returned. * * @global wpdb $wpdb WordPress database abstraction object. * * @since 2.3.0 * * @param string $term The term name to add. * @param string $taxonomy The taxonomy to which to add the term. * @param array|string $args { * Optional. Array or query string of arguments for inserting a term. * * @type string $alias_of Slug of the term to make this term an alias of. * Default empty string. Accepts a term slug. * @type string $description The term description. Default empty string. * @type int $parent The id of the parent term. Default 0. * @type string $slug The term slug to use. Default empty string. * } * @return array|WP_Error { * An array of the new term data, WP_Error otherwise. * * @type int $term_id The new term ID. * @type int|string $term_taxonomy_id The new term taxonomy ID. Can be a numeric string. * } */ function wp_insert_term( $term, $taxonomy, $args = array() ) { global $wpdb; if ( ! taxonomy_exists( $taxonomy ) ) { return new WP_Error( 'invalid_taxonomy', __( 'Invalid taxonomy.' ) ); } /** * Filters a term before it is sanitized and inserted into the database. * * @since 3.0.0 * @since 6.1.0 The `$args` parameter was added. * * @param string|WP_Error $term The term name to add, or a WP_Error object if there's an error. * @param string $taxonomy Taxonomy slug. * @param array|string $args Array or query string of arguments passed to wp_insert_term(). */ $term = apply_filters( 'pre_insert_term', $term, $taxonomy, $args ); if ( is_wp_error( $term ) ) { return $term; } if ( is_int( $term ) && 0 === $term ) { return new WP_Error( 'invalid_term_id', __( 'Invalid term ID.' ) ); } if ( '' === trim( $term ) ) { return new WP_Error( 'empty_term_name', __( 'A name is required for this term.' ) ); } $defaults = array( 'alias_of' => '', 'description' => '', 'parent' => 0, 'slug' => '', ); $args = wp_parse_args( $args, $defaults ); if ( (int) $args['parent'] > 0 && ! term_exists( (int) $args['parent'] ) ) { return new WP_Error( 'missing_parent', __( 'Parent term does not exist.' ) ); } $args['name'] = $term; $args['taxonomy'] = $taxonomy; // Coerce null description to strings, to avoid database errors. $args['description'] = (string) $args['description']; $args = sanitize_term( $args, $taxonomy, 'db' ); // expected_slashed ($name) $name = wp_unslash( $args['name'] ); $description = wp_unslash( $args['description'] ); $parent = (int) $args['parent']; $slug_provided = ! empty( $args['slug'] ); if ( ! $slug_provided ) { $slug = sanitize_title( $name ); } else { $slug = $args['slug']; } $term_group = 0; if ( $args['alias_of'] ) { $alias = get_term_by( 'slug', $args['alias_of'], $taxonomy ); if ( ! empty( $alias->term_group ) ) { // The alias we want is already in a group, so let's use that one. $term_group = $alias->term_group; } elseif ( ! empty( $alias->term_id ) ) { /* * The alias is not in a group, so we create a new one * and add the alias to it. */ $term_group = $wpdb->get_var( "SELECT MAX(term_group) FROM $wpdb->terms" ) + 1; wp_update_term( $alias->term_id, $taxonomy, array( 'term_group' => $term_group, ) ); } } /* * Prevent the creation of terms with duplicate names at the same level of a taxonomy hierarchy, * unless a unique slug has been explicitly provided. */ $name_matches = get_terms( array( 'taxonomy' => $taxonomy, 'name' => $name, 'hide_empty' => false, 'parent' => $args['parent'], 'update_term_meta_cache' => false, ) ); /* * The `name` match in `get_terms()` doesn't differentiate accented characters, * so we do a stricter comparison here. */ $name_match = null; if ( $name_matches ) { foreach ( $name_matches as $_match ) { if ( strtolower( $name ) === strtolower( $_match->name ) ) { $name_match = $_match; break; } } } if ( $name_match ) { $slug_match = get_term_by( 'slug', $slug, $taxonomy ); if ( ! $slug_provided || $name_match->slug === $slug || $slug_match ) { if ( is_taxonomy_hierarchical( $taxonomy ) ) { $siblings = get_terms( array( 'taxonomy' => $taxonomy, 'get' => 'all', 'parent' => $parent, 'update_term_meta_cache' => false, ) ); $existing_term = null; $sibling_names = wp_list_pluck( $siblings, 'name' ); $sibling_slugs = wp_list_pluck( $siblings, 'slug' ); if ( ( ! $slug_provided || $name_match->slug === $slug ) && in_array( $name, $sibling_names, true ) ) { $existing_term = $name_match; } elseif ( $slug_match && in_array( $slug, $sibling_slugs, true ) ) { $existing_term = $slug_match; } if ( $existing_term ) { return new WP_Error( 'term_exists', __( 'A term with the name provided already exists with this parent.' ), $existing_term->term_id ); } } else { return new WP_Error( 'term_exists', __( 'A term with the name provided already exists in this taxonomy.' ), $name_match->term_id ); } } } $slug = wp_unique_term_slug( $slug, (object) $args ); $data = compact( 'name', 'slug', 'term_group' ); /** * Filters term data before it is inserted into the database. * * @since 4.7.0 * * @param array $data Term data to be inserted. * @param string $taxonomy Taxonomy slug. * @param array $args Arguments passed to wp_insert_term(). */ $data = apply_filters( 'wp_insert_term_data', $data, $taxonomy, $args ); if ( false === $wpdb->insert( $wpdb->terms, $data ) ) { return new WP_Error( 'db_insert_error', __( 'Could not insert term into the database.' ), $wpdb->last_error ); } $term_id = (int) $wpdb->insert_id; // Seems unreachable. However, is used in the case that a term name is provided, which sanitizes to an empty string. if ( empty( $slug ) ) { $slug = sanitize_title( $slug, $term_id ); /** This action is documented in wp-includes/taxonomy.php */ do_action( 'edit_terms', $term_id, $taxonomy ); $wpdb->update( $wpdb->terms, compact( 'slug' ), compact( 'term_id' ) ); /** This action is documented in wp-includes/taxonomy.php */ do_action( 'edited_terms', $term_id, $taxonomy ); } $tt_id = $wpdb->get_var( $wpdb->prepare( "SELECT tt.term_taxonomy_id FROM $wpdb->term_taxonomy AS tt INNER JOIN $wpdb->terms AS t ON tt.term_id = t.term_id WHERE tt.taxonomy = %s AND t.term_id = %d", $taxonomy, $term_id ) ); if ( ! empty( $tt_id ) ) { return array( 'term_id' => $term_id, 'term_taxonomy_id' => $tt_id, ); } if ( false === $wpdb->insert( $wpdb->term_taxonomy, compact( 'term_id', 'taxonomy', 'description', 'parent' ) + array( 'count' => 0 ) ) ) { return new WP_Error( 'db_insert_error', __( 'Could not insert term taxonomy into the database.' ), $wpdb->last_error ); } $tt_id = (int) $wpdb->insert_id; /* * Sanity check: if we just created a term with the same parent + taxonomy + slug but a higher term_id than * an existing term, then we have unwittingly created a duplicate term. Delete the dupe, and use the term_id * and term_taxonomy_id of the older term instead. Then return out of the function so that the "create" hooks * are not fired. */ $duplicate_term = $wpdb->get_row( $wpdb->prepare( "SELECT t.term_id, t.slug, tt.term_taxonomy_id, tt.taxonomy FROM $wpdb->terms AS t INNER JOIN $wpdb->term_taxonomy AS tt ON ( tt.term_id = t.term_id ) WHERE t.slug = %s AND tt.parent = %d AND tt.taxonomy = %s AND t.term_id < %d AND tt.term_taxonomy_id != %d", $slug, $parent, $taxonomy, $term_id, $tt_id ) ); /** * Filters the duplicate term check that takes place during term creation. * * Term parent + taxonomy + slug combinations are meant to be unique, and wp_insert_term() * performs a last-minute confirmation of this uniqueness before allowing a new term * to be created. Plugins with different uniqueness requirements may use this filter * to bypass or modify the duplicate-term check. * * @since 5.1.0 * * @param object $duplicate_term Duplicate term row from terms table, if found. * @param string $term Term being inserted. * @param string $taxonomy Taxonomy name. * @param array $args Arguments passed to wp_insert_term(). * @param int $tt_id term_taxonomy_id for the newly created term. */ $duplicate_term = apply_filters( 'wp_insert_term_duplicate_term_check', $duplicate_term, $term, $taxonomy, $args, $tt_id ); if ( $duplicate_term ) { $wpdb->delete( $wpdb->terms, array( 'term_id' => $term_id ) ); $wpdb->delete( $wpdb->term_taxonomy, array( 'term_taxonomy_id' => $tt_id ) ); $term_id = (int) $duplicate_term->term_id; $tt_id = (int) $duplicate_term->term_taxonomy_id; clean_term_cache( $term_id, $taxonomy ); return array( 'term_id' => $term_id, 'term_taxonomy_id' => $tt_id, ); } /** * Fires immediately after a new term is created, before the term cache is cleaned. * * The {@see 'create_$taxonomy'} hook is also available for targeting a specific * taxonomy. * * @since 2.3.0 * @since 6.1.0 The `$args` parameter was added. * * @param int $term_id Term ID. * @param int $tt_id Term taxonomy ID. * @param string $taxonomy Taxonomy slug. * @param array $args Arguments passed to wp_insert_term(). */ do_action( 'create_term', $term_id, $tt_id, $taxonomy, $args ); /** * Fires after a new term is created for a specific taxonomy. * * The dynamic portion of the hook name, `$taxonomy`, refers * to the slug of the taxonomy the term was created for. * * Possible hook names include: * * - `create_category` * - `create_post_tag` * * @since 2.3.0 * @since 6.1.0 The `$args` parameter was added. * * @param int $term_id Term ID. * @param int $tt_id Term taxonomy ID. * @param array $args Arguments passed to wp_insert_term(). */ do_action( "create_{$taxonomy}", $term_id, $tt_id, $args ); /** * Filters the term ID after a new term is created. * * @since 2.3.0 * @since 6.1.0 The `$args` parameter was added. * * @param int $term_id Term ID. * @param int $tt_id Term taxonomy ID. * @param array $args Arguments passed to wp_insert_term(). */ $term_id = apply_filters( 'term_id_filter', $term_id, $tt_id, $args ); clean_term_cache( $term_id, $taxonomy ); /** * Fires after a new term is created, and after the term cache has been cleaned. * * The {@see 'created_$taxonomy'} hook is also available for targeting a specific * taxonomy. * * @since 2.3.0 * @since 6.1.0 The `$args` parameter was added. * * @param int $term_id Term ID. * @param int $tt_id Term taxonomy ID. * @param string $taxonomy Taxonomy slug. * @param array $args Arguments passed to wp_insert_term(). */ do_action( 'created_term', $term_id, $tt_id, $taxonomy, $args ); /** * Fires after a new term in a specific taxonomy is created, and after the term * cache has been cleaned. * * The dynamic portion of the hook name, `$taxonomy`, refers to the taxonomy slug. * * Possible hook names include: * * - `created_category` * - `created_post_tag` * * @since 2.3.0 * @since 6.1.0 The `$args` parameter was added. * * @param int $term_id Term ID. * @param int $tt_id Term taxonomy ID. * @param array $args Arguments passed to wp_insert_term(). */ do_action( "created_{$taxonomy}", $term_id, $tt_id, $args ); /** * Fires after a term has been saved, and the term cache has been cleared. * * The {@see 'saved_$taxonomy'} hook is also available for targeting a specific * taxonomy. * * @since 5.5.0 * @since 6.1.0 The `$args` parameter was added. * * @param int $term_id Term ID. * @param int $tt_id Term taxonomy ID. * @param string $taxonomy Taxonomy slug. * @param bool $update Whether this is an existing term being updated. * @param array $args Arguments passed to wp_insert_term(). */ do_action( 'saved_term', $term_id, $tt_id, $taxonomy, false, $args ); /** * Fires after a term in a specific taxonomy has been saved, and the term * cache has been cleared. * * The dynamic portion of the hook name, `$taxonomy`, refers to the taxonomy slug. * * Possible hook names include: * * - `saved_category` * - `saved_post_tag` * * @since 5.5.0 * @since 6.1.0 The `$args` parameter was added. * * @param int $term_id Term ID. * @param int $tt_id Term taxonomy ID. * @param bool $update Whether this is an existing term being updated. * @param array $args Arguments passed to wp_insert_term(). */ do_action( "saved_{$taxonomy}", $term_id, $tt_id, false, $args ); return array( 'term_id' => $term_id, 'term_taxonomy_id' => $tt_id, ); } /** * Creates term and taxonomy relationships. * * Relates an object (post, link, etc.) to a term and taxonomy type. Creates the * term and taxonomy relationship if it doesn't already exist. Creates a term if * it doesn't exist (using the slug). * * A relationship means that the term is grouped in or belongs to the taxonomy. * A term has no meaning until it is given context by defining which taxonomy it * exists under. * * @since 2.3.0 * * @global wpdb $wpdb WordPress database abstraction object. * * @param int $object_id The object to relate to. * @param string|int|array $terms A single term slug, single term ID, or array of either term slugs or IDs. * Will replace all existing related terms in this taxonomy. Passing an * empty array will remove all related terms. * @param string $taxonomy The context in which to relate the term to the object. * @param bool $append Optional. If false will delete difference of terms. Default false. * @return array|WP_Error Term taxonomy IDs of the affected terms or WP_Error on failure. */ function wp_set_object_terms( $object_id, $terms, $taxonomy, $append = false ) { global $wpdb; $object_id = (int) $object_id; if ( ! taxonomy_exists( $taxonomy ) ) { return new WP_Error( 'invalid_taxonomy', __( 'Invalid taxonomy.' ) ); } if ( empty( $terms ) ) { $terms = array(); } elseif ( ! is_array( $terms ) ) { $terms = array( $terms ); } if ( ! $append ) { $old_tt_ids = wp_get_object_terms( $object_id, $taxonomy, array( 'fields' => 'tt_ids', 'orderby' => 'none', 'update_term_meta_cache' => false, ) ); } else { $old_tt_ids = array(); } $tt_ids = array(); $term_ids = array(); $new_tt_ids = array(); foreach ( (array) $terms as $term ) { if ( '' === trim( $term ) ) { continue; } $term_info = term_exists( $term, $taxonomy ); if ( ! $term_info ) { // Skip if a non-existent term ID is passed. if ( is_int( $term ) ) { continue; } $term_info = wp_insert_term( $term, $taxonomy ); } if ( is_wp_error( $term_info ) ) { return $term_info; } $term_ids[] = $term_info['term_id']; $tt_id = $term_info['term_taxonomy_id']; $tt_ids[] = $tt_id; if ( $wpdb->get_var( $wpdb->prepare( "SELECT term_taxonomy_id FROM $wpdb->term_relationships WHERE object_id = %d AND term_taxonomy_id = %d", $object_id, $tt_id ) ) ) { continue; } /** * Fires immediately before an object-term relationship is added. * * @since 2.9.0 * @since 4.7.0 Added the `$taxonomy` parameter. * * @param int $object_id Object ID. * @param int $tt_id Term taxonomy ID. * @param string $taxonomy Taxonomy slug. */ do_action( 'add_term_relationship', $object_id, $tt_id, $taxonomy ); $wpdb->insert( $wpdb->term_relationships, array( 'object_id' => $object_id, 'term_taxonomy_id' => $tt_id, ) ); /** * Fires immediately after an object-term relationship is added. * * @since 2.9.0 * @since 4.7.0 Added the `$taxonomy` parameter. * * @param int $object_id Object ID. * @param int $tt_id Term taxonomy ID. * @param string $taxonomy Taxonomy slug. */ do_action( 'added_term_relationship', $object_id, $tt_id, $taxonomy ); $new_tt_ids[] = $tt_id; } if ( $new_tt_ids ) { wp_update_term_count( $new_tt_ids, $taxonomy ); } if ( ! $append ) { $delete_tt_ids = array_diff( $old_tt_ids, $tt_ids ); if ( $delete_tt_ids ) { $in_delete_tt_ids = "'" . implode( "', '", $delete_tt_ids ) . "'"; $delete_term_ids = $wpdb->get_col( $wpdb->prepare( "SELECT tt.term_id FROM $wpdb->term_taxonomy AS tt WHERE tt.taxonomy = %s AND tt.term_taxonomy_id IN ($in_delete_tt_ids)", $taxonomy ) ); $delete_term_ids = array_map( 'intval', $delete_term_ids ); $remove = wp_remove_object_terms( $object_id, $delete_term_ids, $taxonomy ); if ( is_wp_error( $remove ) ) { return $remove; } } } $t = get_taxonomy( $taxonomy ); if ( ! $append && isset( $t->sort ) && $t->sort ) { $values = array(); $term_order = 0; $final_tt_ids = wp_get_object_terms( $object_id, $taxonomy, array( 'fields' => 'tt_ids', 'update_term_meta_cache' => false, ) ); foreach ( $tt_ids as $tt_id ) { if ( in_array( (int) $tt_id, $final_tt_ids, true ) ) { $values[] = $wpdb->prepare( '(%d, %d, %d)', $object_id, $tt_id, ++$term_order ); } } if ( $values ) { if ( false === $wpdb->query( "INSERT INTO $wpdb->term_relationships (object_id, term_taxonomy_id, term_order) VALUES " . implode( ',', $values ) . ' ON DUPLICATE KEY UPDATE term_order = VALUES(term_order)' ) ) { return new WP_Error( 'db_insert_error', __( 'Could not insert term relationship into the database.' ), $wpdb->last_error ); } } } wp_cache_delete( $object_id, $taxonomy . '_relationships' ); wp_cache_set_terms_last_changed(); /** * Fires after an object's terms have been set. * * @since 2.8.0 * * @param int $object_id Object ID. * @param array $terms An array of object term IDs or slugs. * @param array $tt_ids An array of term taxonomy IDs. * @param string $taxonomy Taxonomy slug. * @param bool $append Whether to append new terms to the old terms. * @param array $old_tt_ids Old array of term taxonomy IDs. */ do_action( 'set_object_terms', $object_id, $terms, $tt_ids, $taxonomy, $append, $old_tt_ids ); return $tt_ids; } /** * Adds term(s) associated with a given object. * * @since 3.6.0 * * @param int $object_id The ID of the object to which the terms will be added. * @param string|int|array $terms The slug(s) or ID(s) of the term(s) to add. * @param array|string $taxonomy Taxonomy name. * @return array|WP_Error Term taxonomy IDs of the affected terms. */ function wp_add_object_terms( $object_id, $terms, $taxonomy ) { return wp_set_object_terms( $object_id, $terms, $taxonomy, true ); } /** * Removes term(s) associated with a given object. * * @since 3.6.0 * * @global wpdb $wpdb WordPress database abstraction object. * * @param int $object_id The ID of the object from which the terms will be removed. * @param string|int|array $terms The slug(s) or ID(s) of the term(s) to remove. * @param string $taxonomy Taxonomy name. * @return bool|WP_Error True on success, false or WP_Error on failure. */ function wp_remove_object_terms( $object_id, $terms, $taxonomy ) { global $wpdb; $object_id = (int) $object_id; if ( ! taxonomy_exists( $taxonomy ) ) { return new WP_Error( 'invalid_taxonomy', __( 'Invalid taxonomy.' ) ); } if ( ! is_array( $terms ) ) { $terms = array( $terms ); } $tt_ids = array(); foreach ( (array) $terms as $term ) { if ( '' === trim( $term ) ) { continue; } $term_info = term_exists( $term, $taxonomy ); if ( ! $term_info ) { // Skip if a non-existent term ID is passed. if ( is_int( $term ) ) { continue; } } if ( is_wp_error( $term_info ) ) { return $term_info; } $tt_ids[] = $term_info['term_taxonomy_id']; } if ( $tt_ids ) { $in_tt_ids = "'" . implode( "', '", $tt_ids ) . "'"; /** * Fires immediately before an object-term relationship is deleted. * * @since 2.9.0 * @since 4.7.0 Added the `$taxonomy` parameter. * * @param int $object_id Object ID. * @param array $tt_ids An array of term taxonomy IDs. * @param string $taxonomy Taxonomy slug. */ do_action( 'delete_term_relationships', $object_id, $tt_ids, $taxonomy ); $deleted = $wpdb->query( $wpdb->prepare( "DELETE FROM $wpdb->term_relationships WHERE object_id = %d AND term_taxonomy_id IN ($in_tt_ids)", $object_id ) ); wp_cache_delete( $object_id, $taxonomy . '_relationships' ); wp_cache_set_terms_last_changed(); /** * Fires immediately after an object-term relationship is deleted. * * @since 2.9.0 * @since 4.7.0 Added the `$taxonomy` parameter. * * @param int $object_id Object ID. * @param array $tt_ids An array of term taxonomy IDs. * @param string $taxonomy Taxonomy slug. */ do_action( 'deleted_term_relationships', $object_id, $tt_ids, $taxonomy ); wp_update_term_count( $tt_ids, $taxonomy ); return (bool) $deleted; } return false; } /** * Makes term slug unique, if it isn't already. * * The `$slug` has to be unique global to every taxonomy, meaning that one * taxonomy term can't have a matching slug with another taxonomy term. Each * slug has to be globally unique for every taxonomy. * * The way this works is that if the taxonomy that the term belongs to is * hierarchical and has a parent, it will append that parent to the $slug. * * If that still doesn't return a unique slug, then it tries to append a number * until it finds a number that is truly unique. * * The only purpose for `$term` is for appending a parent, if one exists. * * @since 2.3.0 * * @global wpdb $wpdb WordPress database abstraction object. * * @param string $slug The string that will be tried for a unique slug. * @param object $term The term object that the `$slug` will belong to. * @return string Will return a true unique slug. */ function wp_unique_term_slug( $slug, $term ) { global $wpdb; $needs_suffix = true; $original_slug = $slug; // As of 4.1, duplicate slugs are allowed as long as they're in different taxonomies. if ( ! term_exists( $slug ) || get_option( 'db_version' ) >= 30133 && ! get_term_by( 'slug', $slug, $term->taxonomy ) ) { $needs_suffix = false; } /* * If the taxonomy supports hierarchy and the term has a parent, make the slug unique * by incorporating parent slugs. */ $parent_suffix = ''; if ( $needs_suffix && is_taxonomy_hierarchical( $term->taxonomy ) && ! empty( $term->parent ) ) { $the_parent = $term->parent; while ( ! empty( $the_parent ) ) { $parent_term = get_term( $the_parent, $term->taxonomy ); if ( is_wp_error( $parent_term ) || empty( $parent_term ) ) { break; } $parent_suffix .= '-' . $parent_term->slug; if ( ! term_exists( $slug . $parent_suffix ) ) { break; } if ( empty( $parent_term->parent ) ) { break; } $the_parent = $parent_term->parent; } } // If we didn't get a unique slug, try appending a number to make it unique. /** * Filters whether the proposed unique term slug is bad. * * @since 4.3.0 * * @param bool $needs_suffix Whether the slug needs to be made unique with a suffix. * @param string $slug The slug. * @param object $term Term object. */ if ( apply_filters( 'wp_unique_term_slug_is_bad_slug', $needs_suffix, $slug, $term ) ) { if ( $parent_suffix ) { $slug .= $parent_suffix; } if ( ! empty( $term->term_id ) ) { $query = $wpdb->prepare( "SELECT slug FROM $wpdb->terms WHERE slug = %s AND term_id != %d", $slug, $term->term_id ); } else { $query = $wpdb->prepare( "SELECT slug FROM $wpdb->terms WHERE slug = %s", $slug ); } if ( $wpdb->get_var( $query ) ) { // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared $num = 2; do { $alt_slug = $slug . "-$num"; ++$num; $slug_check = $wpdb->get_var( $wpdb->prepare( "SELECT slug FROM $wpdb->terms WHERE slug = %s", $alt_slug ) ); } while ( $slug_check ); $slug = $alt_slug; } } /** * Filters the unique term slug. * * @since 4.3.0 * * @param string $slug Unique term slug. * @param object $term Term object. * @param string $original_slug Slug originally passed to the function for testing. */ return apply_filters( 'wp_unique_term_slug', $slug, $term, $original_slug ); } /** * Updates term based on arguments provided. * * The `$args` will indiscriminately override all values with the same field name. * Care must be taken to not override important information need to update or * update will fail (or perhaps create a new term, neither would be acceptable). * * Defaults will set 'alias_of', 'description', 'parent', and 'slug' if not * defined in `$args` already. * * 'alias_of' will create a term group, if it doesn't already exist, and * update it for the `$term`. * * If the 'slug' argument in `$args` is missing, then the 'name' will be used. * If you set 'slug' and it isn't unique, then a WP_Error is returned. * If you don't pass any slug, then a unique one will be created. * * @since 2.3.0 * * @global wpdb $wpdb WordPress database abstraction object. * * @param int $term_id The ID of the term. * @param string $taxonomy The taxonomy of the term. * @param array $args { * Optional. Array of arguments for updating a term. * * @type string $alias_of Slug of the term to make this term an alias of. * Default empty string. Accepts a term slug. * @type string $description The term description. Default empty string. * @type int $parent The id of the parent term. Default 0. * @type string $slug The term slug to use. Default empty string. * } * @return array|WP_Error An array containing the `term_id` and `term_taxonomy_id`, * WP_Error otherwise. */ function wp_update_term( $term_id, $taxonomy, $args = array() ) { global $wpdb; if ( ! taxonomy_exists( $taxonomy ) ) { return new WP_Error( 'invalid_taxonomy', __( 'Invalid taxonomy.' ) ); } $term_id = (int) $term_id; // First, get all of the original args. $term = get_term( $term_id, $taxonomy ); if ( is_wp_error( $term ) ) { return $term; } if ( ! $term ) { return new WP_Error( 'invalid_term', __( 'Empty Term.' ) ); } $term = (array) $term->data; // Escape data pulled from DB. $term = wp_slash( $term ); // Merge old and new args with new args overwriting old ones. $args = array_merge( $term, $args ); $defaults = array( 'alias_of' => '', 'description' => '', 'parent' => 0, 'slug' => '', ); $args = wp_parse_args( $args, $defaults ); $args = sanitize_term( $args, $taxonomy, 'db' ); $parsed_args = $args; // expected_slashed ($name) $name = wp_unslash( $args['name'] ); $description = wp_unslash( $args['description'] ); $parsed_args['name'] = $name; $parsed_args['description'] = $description; if ( '' === trim( $name ) ) { return new WP_Error( 'empty_term_name', __( 'A name is required for this term.' ) ); } if ( (int) $parsed_args['parent'] > 0 && ! term_exists( (int) $parsed_args['parent'] ) ) { return new WP_Error( 'missing_parent', __( 'Parent term does not exist.' ) ); } $empty_slug = false; if ( empty( $args['slug'] ) ) { $empty_slug = true; $slug = sanitize_title( $name ); } else { $slug = $args['slug']; } $parsed_args['slug'] = $slug; $term_group = isset( $parsed_args['term_group'] ) ? $parsed_args['term_group'] : 0; if ( $args['alias_of'] ) { $alias = get_term_by( 'slug', $args['alias_of'], $taxonomy ); if ( ! empty( $alias->term_group ) ) { // The alias we want is already in a group, so let's use that one. $term_group = $alias->term_group; } elseif ( ! empty( $alias->term_id ) ) { /* * The alias is not in a group, so we create a new one * and add the alias to it. */ $term_group = $wpdb->get_var( "SELECT MAX(term_group) FROM $wpdb->terms" ) + 1; wp_update_term( $alias->term_id, $taxonomy, array( 'term_group' => $term_group, ) ); } $parsed_args['term_group'] = $term_group; } /** * Filters the term parent. * * Hook to this filter to see if it will cause a hierarchy loop. * * @since 3.1.0 * * @param int $parent_term ID of the parent term. * @param int $term_id Term ID. * @param string $taxonomy Taxonomy slug. * @param array $parsed_args An array of potentially altered update arguments for the given term. * @param array $args Arguments passed to wp_update_term(). */ $parent = (int) apply_filters( 'wp_update_term_parent', $args['parent'], $term_id, $taxonomy, $parsed_args, $args ); // Check for duplicate slug. $duplicate = get_term_by( 'slug', $slug, $taxonomy ); if ( $duplicate && $duplicate->term_id !== $term_id ) { /* * If an empty slug was passed or the parent changed, reset the slug to something unique. * Otherwise, bail. */ if ( $empty_slug || ( $parent !== (int) $term['parent'] ) ) { $slug = wp_unique_term_slug( $slug, (object) $args ); } else { /* translators: %s: Taxonomy term slug. */ return new WP_Error( 'duplicate_term_slug', sprintf( __( 'The slug “%s” is already in use by another term.' ), $slug ) ); } } $tt_id = (int) $wpdb->get_var( $wpdb->prepare( "SELECT tt.term_taxonomy_id FROM $wpdb->term_taxonomy AS tt INNER JOIN $wpdb->terms AS t ON tt.term_id = t.term_id WHERE tt.taxonomy = %s AND t.term_id = %d", $taxonomy, $term_id ) ); // Check whether this is a shared term that needs splitting. $_term_id = _split_shared_term( $term_id, $tt_id ); if ( ! is_wp_error( $_term_id ) ) { $term_id = $_term_id; } /** * Fires immediately before the given terms are edited. * * @since 2.9.0 * @since 6.1.0 The `$args` parameter was added. * * @param int $term_id Term ID. * @param string $taxonomy Taxonomy slug. * @param array $args Arguments passed to wp_update_term(). */ do_action( 'edit_terms', $term_id, $taxonomy, $args ); $data = compact( 'name', 'slug', 'term_group' ); /** * Filters term data before it is updated in the database. * * @since 4.7.0 * * @param array $data Term data to be updated. * @param int $term_id Term ID. * @param string $taxonomy Taxonomy slug. * @param array $args Arguments passed to wp_update_term(). */ $data = apply_filters( 'wp_update_term_data', $data, $term_id, $taxonomy, $args ); $wpdb->update( $wpdb->terms, $data, compact( 'term_id' ) ); if ( empty( $slug ) ) { $slug = sanitize_title( $name, $term_id ); $wpdb->update( $wpdb->terms, compact( 'slug' ), compact( 'term_id' ) ); } /** * Fires immediately after a term is updated in the database, but before its * term-taxonomy relationship is updated. * * @since 2.9.0 * @since 6.1.0 The `$args` parameter was added. * * @param int $term_id Term ID. * @param string $taxonomy Taxonomy slug. * @param array $args Arguments passed to wp_update_term(). */ do_action( 'edited_terms', $term_id, $taxonomy, $args ); /** * Fires immediate before a term-taxonomy relationship is updated. * * @since 2.9.0 * @since 6.1.0 The `$args` parameter was added. * * @param int $tt_id Term taxonomy ID. * @param string $taxonomy Taxonomy slug. * @param array $args Arguments passed to wp_update_term(). */ do_action( 'edit_term_taxonomy', $tt_id, $taxonomy, $args ); $wpdb->update( $wpdb->term_taxonomy, compact( 'term_id', 'taxonomy', 'description', 'parent' ), array( 'term_taxonomy_id' => $tt_id ) ); /** * Fires immediately after a term-taxonomy relationship is updated. * * @since 2.9.0 * @since 6.1.0 The `$args` parameter was added. * * @param int $tt_id Term taxonomy ID. * @param string $taxonomy Taxonomy slug. * @param array $args Arguments passed to wp_update_term(). */ do_action( 'edited_term_taxonomy', $tt_id, $taxonomy, $args ); /** * Fires after a term has been updated, but before the term cache has been cleaned. * * The {@see 'edit_$taxonomy'} hook is also available for targeting a specific * taxonomy. * * @since 2.3.0 * @since 6.1.0 The `$args` parameter was added. * * @param int $term_id Term ID. * @param int $tt_id Term taxonomy ID. * @param string $taxonomy Taxonomy slug. * @param array $args Arguments passed to wp_update_term(). */ do_action( 'edit_term', $term_id, $tt_id, $taxonomy, $args ); /** * Fires after a term in a specific taxonomy has been updated, but before the term * cache has been cleaned. * * The dynamic portion of the hook name, `$taxonomy`, refers to the taxonomy slug. * * Possible hook names include: * * - `edit_category` * - `edit_post_tag` * * @since 2.3.0 * @since 6.1.0 The `$args` parameter was added. * * @param int $term_id Term ID. * @param int $tt_id Term taxonomy ID. * @param array $args Arguments passed to wp_update_term(). */ do_action( "edit_{$taxonomy}", $term_id, $tt_id, $args ); /** This filter is documented in wp-includes/taxonomy.php */ $term_id = apply_filters( 'term_id_filter', $term_id, $tt_id ); clean_term_cache( $term_id, $taxonomy ); /** * Fires after a term has been updated, and the term cache has been cleaned. * * The {@see 'edited_$taxonomy'} hook is also available for targeting a specific * taxonomy. * * @since 2.3.0 * @since 6.1.0 The `$args` parameter was added. * * @param int $term_id Term ID. * @param int $tt_id Term taxonomy ID. * @param string $taxonomy Taxonomy slug. * @param array $args Arguments passed to wp_update_term(). */ do_action( 'edited_term', $term_id, $tt_id, $taxonomy, $args ); /** * Fires after a term for a specific taxonomy has been updated, and the term * cache has been cleaned. * * The dynamic portion of the hook name, `$taxonomy`, refers to the taxonomy slug. * * Possible hook names include: * * - `edited_category` * - `edited_post_tag` * * @since 2.3.0 * @since 6.1.0 The `$args` parameter was added. * * @param int $term_id Term ID. * @param int $tt_id Term taxonomy ID. * @param array $args Arguments passed to wp_update_term(). */ do_action( "edited_{$taxonomy}", $term_id, $tt_id, $args ); /** This action is documented in wp-includes/taxonomy.php */ do_action( 'saved_term', $term_id, $tt_id, $taxonomy, true, $args ); /** This action is documented in wp-includes/taxonomy.php */ do_action( "saved_{$taxonomy}", $term_id, $tt_id, true, $args ); return array( 'term_id' => $term_id, 'term_taxonomy_id' => $tt_id, ); } /** * Enables or disables term counting. * * @since 2.5.0 * * @param bool $defer Optional. Enable if true, disable if false. * @return bool Whether term counting is enabled or disabled. */ function wp_defer_term_counting( $defer = null ) { static $_defer = false; if ( is_bool( $defer ) ) { $_defer = $defer; // Flush any deferred counts. if ( ! $defer ) { wp_update_term_count( null, null, true ); } } return $_defer; } /** * Updates the amount of terms in taxonomy. * * If there is a taxonomy callback applied, then it will be called for updating * the count. * * The default action is to count what the amount of terms have the relationship * of term ID. Once that is done, then update the database. * * @since 2.3.0 * * @param int|array $terms The term_taxonomy_id of the terms. * @param string $taxonomy The context of the term. * @param bool $do_deferred Whether to flush the deferred term counts too. Default false. * @return bool If no terms will return false, and if successful will return true. */ function wp_update_term_count( $terms, $taxonomy, $do_deferred = false ) { static $_deferred = array(); if ( $do_deferred ) { foreach ( (array) array_keys( $_deferred ) as $tax ) { wp_update_term_count_now( $_deferred[ $tax ], $tax ); unset( $_deferred[ $tax ] ); } } if ( empty( $terms ) ) { return false; } if ( ! is_array( $terms ) ) { $terms = array( $terms ); } if ( wp_defer_term_counting() ) { if ( ! isset( $_deferred[ $taxonomy ] ) ) { $_deferred[ $taxonomy ] = array(); } $_deferred[ $taxonomy ] = array_unique( array_merge( $_deferred[ $taxonomy ], $terms ) ); return true; } return wp_update_term_count_now( $terms, $taxonomy ); } /** * Performs term count update immediately. * * @since 2.5.0 * * @param array $terms The term_taxonomy_id of terms to update. * @param string $taxonomy The context of the term. * @return true Always true when complete. */ function wp_update_term_count_now( $terms, $taxonomy ) { $terms = array_map( 'intval', $terms ); $taxonomy = get_taxonomy( $taxonomy ); if ( ! empty( $taxonomy->update_count_callback ) ) { call_user_func( $taxonomy->update_count_callback, $terms, $taxonomy ); } else { $object_types = (array) $taxonomy->object_type; foreach ( $object_types as &$object_type ) { if ( str_starts_with( $object_type, 'attachment:' ) ) { list( $object_type ) = explode( ':', $object_type ); } } if ( array_filter( $object_types, 'post_type_exists' ) == $object_types ) { // Only post types are attached to this taxonomy. _update_post_term_count( $terms, $taxonomy ); } else { // Default count updater. _update_generic_term_count( $terms, $taxonomy ); } } clean_term_cache( $terms, '', false ); return true; } // // Cache. // /** * Removes the taxonomy relationship to terms from the cache. * * Will remove the entire taxonomy relationship containing term `$object_id`. The * term IDs have to exist within the taxonomy `$object_type` for the deletion to * take place. * * @since 2.3.0 * * @global bool $_wp_suspend_cache_invalidation * * @see get_object_taxonomies() for more on $object_type. * * @param int|array $object_ids Single or list of term object ID(s). * @param array|string $object_type The taxonomy object type. */ function clean_object_term_cache( $object_ids, $object_type ) { global $_wp_suspend_cache_invalidation; if ( ! empty( $_wp_suspend_cache_invalidation ) ) { return; } if ( ! is_array( $object_ids ) ) { $object_ids = array( $object_ids ); } $taxonomies = get_object_taxonomies( $object_type ); foreach ( $taxonomies as $taxonomy ) { wp_cache_delete_multiple( $object_ids, "{$taxonomy}_relationships" ); } wp_cache_set_terms_last_changed(); /** * Fires after the object term cache has been cleaned. * * @since 2.5.0 * * @param array $object_ids An array of object IDs. * @param string $object_type Object type. */ do_action( 'clean_object_term_cache', $object_ids, $object_type ); } /** * Removes all of the term IDs from the cache. * * @since 2.3.0 * * @global wpdb $wpdb WordPress database abstraction object. * @global bool $_wp_suspend_cache_invalidation * * @param int|int[] $ids Single or array of term IDs. * @param string $taxonomy Optional. Taxonomy slug. Can be empty, in which case the taxonomies of the passed * term IDs will be used. Default empty. * @param bool $clean_taxonomy Optional. Whether to clean taxonomy wide caches (true), or just individual * term object caches (false). Default true. */ function clean_term_cache( $ids, $taxonomy = '', $clean_taxonomy = true ) { global $wpdb, $_wp_suspend_cache_invalidation; if ( ! empty( $_wp_suspend_cache_invalidation ) ) { return; } if ( ! is_array( $ids ) ) { $ids = array( $ids ); } $taxonomies = array(); // If no taxonomy, assume tt_ids. if ( empty( $taxonomy ) ) { $tt_ids = array_map( 'intval', $ids ); $tt_ids = implode( ', ', $tt_ids ); $terms = $wpdb->get_results( "SELECT term_id, taxonomy FROM $wpdb->term_taxonomy WHERE term_taxonomy_id IN ($tt_ids)" ); $ids = array(); foreach ( (array) $terms as $term ) { $taxonomies[] = $term->taxonomy; $ids[] = $term->term_id; } wp_cache_delete_multiple( $ids, 'terms' ); $taxonomies = array_unique( $taxonomies ); } else { wp_cache_delete_multiple( $ids, 'terms' ); $taxonomies = array( $taxonomy ); } foreach ( $taxonomies as $taxonomy ) { if ( $clean_taxonomy ) { clean_taxonomy_cache( $taxonomy ); } /** * Fires once after each taxonomy's term cache has been cleaned. * * @since 2.5.0 * @since 4.5.0 Added the `$clean_taxonomy` parameter. * * @param array $ids An array of term IDs. * @param string $taxonomy Taxonomy slug. * @param bool $clean_taxonomy Whether or not to clean taxonomy-wide caches */ do_action( 'clean_term_cache', $ids, $taxonomy, $clean_taxonomy ); } wp_cache_set_terms_last_changed(); } /** * Cleans the caches for a taxonomy. * * @since 4.9.0 * * @param string $taxonomy Taxonomy slug. */ function clean_taxonomy_cache( $taxonomy ) { wp_cache_delete( 'all_ids', $taxonomy ); wp_cache_delete( 'get', $taxonomy ); wp_cache_set_terms_last_changed(); // Regenerate cached hierarchy. delete_option( "{$taxonomy}_children" ); _get_term_hierarchy( $taxonomy ); /** * Fires after a taxonomy's caches have been cleaned. * * @since 4.9.0 * * @param string $taxonomy Taxonomy slug. */ do_action( 'clean_taxonomy_cache', $taxonomy ); } /** * Retrieves the cached term objects for the given object ID. * * Upstream functions (like get_the_terms() and is_object_in_term()) are * responsible for populating the object-term relationship cache. The current * function only fetches relationship data that is already in the cache. * * @since 2.3.0 * @since 4.7.0 Returns a `WP_Error` object if there's an error with * any of the matched terms. * * @param int $id Term object ID, for example a post, comment, or user ID. * @param string $taxonomy Taxonomy name. * @return bool|WP_Term[]|WP_Error Array of `WP_Term` objects, if cached. * False if cache is empty for `$taxonomy` and `$id`. * WP_Error if get_term() returns an error object for any term. */ function get_object_term_cache( $id, $taxonomy ) { $_term_ids = wp_cache_get( $id, "{$taxonomy}_relationships" ); // We leave the priming of relationship caches to upstream functions. if ( false === $_term_ids ) { return false; } // Backward compatibility for if a plugin is putting objects into the cache, rather than IDs. $term_ids = array(); foreach ( $_term_ids as $term_id ) { if ( is_numeric( $term_id ) ) { $term_ids[] = (int) $term_id; } elseif ( isset( $term_id->term_id ) ) { $term_ids[] = (int) $term_id->term_id; } } // Fill the term objects. _prime_term_caches( $term_ids ); $terms = array(); foreach ( $term_ids as $term_id ) { $term = get_term( $term_id, $taxonomy ); if ( is_wp_error( $term ) ) { return $term; } $terms[] = $term; } return $terms; } /** * Updates the cache for the given term object ID(s). * * Note: Due to performance concerns, great care should be taken to only update * term caches when necessary. Processing time can increase exponentially depending * on both the number of passed term IDs and the number of taxonomies those terms * belong to. * * Caches will only be updated for terms not already cached. * * @since 2.3.0 * * @param string|int[] $object_ids Comma-separated list or array of term object IDs. * @param string|string[] $object_type The taxonomy object type or array of the same. * @return void|false Void on success or if the `$object_ids` parameter is empty, * false if all of the terms in `$object_ids` are already cached. */ function update_object_term_cache( $object_ids, $object_type ) { if ( empty( $object_ids ) ) { return; } if ( ! is_array( $object_ids ) ) { $object_ids = explode( ',', $object_ids ); } $object_ids = array_map( 'intval', $object_ids ); $non_cached_ids = array(); $taxonomies = get_object_taxonomies( $object_type ); foreach ( $taxonomies as $taxonomy ) { $cache_values = wp_cache_get_multiple( (array) $object_ids, "{$taxonomy}_relationships" ); foreach ( $cache_values as $id => $value ) { if ( false === $value ) { $non_cached_ids[] = $id; } } } if ( empty( $non_cached_ids ) ) { return false; } $non_cached_ids = array_unique( $non_cached_ids ); $terms = wp_get_object_terms( $non_cached_ids, $taxonomies, array( 'fields' => 'all_with_object_id', 'orderby' => 'name', 'update_term_meta_cache' => false, ) ); $object_terms = array(); foreach ( (array) $terms as $term ) { $object_terms[ $term->object_id ][ $term->taxonomy ][] = $term->term_id; } foreach ( $non_cached_ids as $id ) { foreach ( $taxonomies as $taxonomy ) { if ( ! isset( $object_terms[ $id ][ $taxonomy ] ) ) { if ( ! isset( $object_terms[ $id ] ) ) { $object_terms[ $id ] = array(); } $object_terms[ $id ][ $taxonomy ] = array(); } } } $cache_values = array(); foreach ( $object_terms as $id => $value ) { foreach ( $value as $taxonomy => $terms ) { $cache_values[ $taxonomy ][ $id ] = $terms; } } foreach ( $cache_values as $taxonomy => $data ) { wp_cache_add_multiple( $data, "{$taxonomy}_relationships" ); } } /** * Updates terms in cache. * * @since 2.3.0 * * @param WP_Term[] $terms Array of term objects to change. * @param string $taxonomy Not used. */ function update_term_cache( $terms, $taxonomy = '' ) { $data = array(); foreach ( (array) $terms as $term ) { // Create a copy in case the array was passed by reference. $_term = clone $term; // Object ID should not be cached. unset( $_term->object_id ); $data[ $term->term_id ] = $_term; } wp_cache_add_multiple( $data, 'terms' ); } // // Private. // /** * Retrieves children of taxonomy as term IDs. * * @access private * @since 2.3.0 * * @param string $taxonomy Taxonomy name. * @return array Empty if $taxonomy isn't hierarchical or returns children as term IDs. */ function _get_term_hierarchy( $taxonomy ) { if ( ! is_taxonomy_hierarchical( $taxonomy ) ) { return array(); } $children = get_option( "{$taxonomy}_children" ); if ( is_array( $children ) ) { return $children; } $children = array(); $terms = get_terms( array( 'taxonomy' => $taxonomy, 'get' => 'all', 'orderby' => 'id', 'fields' => 'id=>parent', 'update_term_meta_cache' => false, ) ); foreach ( $terms as $term_id => $parent ) { if ( $parent > 0 ) { $children[ $parent ][] = $term_id; } } update_option( "{$taxonomy}_children", $children ); return $children; } /** * Gets the subset of $terms that are descendants of $term_id. * * If `$terms` is an array of objects, then _get_term_children() returns an array of objects. * If `$terms` is an array of IDs, then _get_term_children() returns an array of IDs. * * @access private * @since 2.3.0 * * @param int $term_id The ancestor term: all returned terms should be descendants of `$term_id`. * @param array $terms The set of terms - either an array of term objects or term IDs - from which those that * are descendants of $term_id will be chosen. * @param string $taxonomy The taxonomy which determines the hierarchy of the terms. * @param array $ancestors Optional. Term ancestors that have already been identified. Passed by reference, to keep * track of found terms when recursing the hierarchy. The array of located ancestors is used * to prevent infinite recursion loops. For performance, `term_ids` are used as array keys, * with 1 as value. Default empty array. * @return array|WP_Error The subset of $terms that are descendants of $term_id. */ function _get_term_children( $term_id, $terms, $taxonomy, &$ancestors = array() ) { $empty_array = array(); if ( empty( $terms ) ) { return $empty_array; } $term_id = (int) $term_id; $term_list = array(); $has_children = _get_term_hierarchy( $taxonomy ); if ( $term_id && ! isset( $has_children[ $term_id ] ) ) { return $empty_array; } // Include the term itself in the ancestors array, so we can properly detect when a loop has occurred. if ( empty( $ancestors ) ) { $ancestors[ $term_id ] = 1; } foreach ( (array) $terms as $term ) { $use_id = false; if ( ! is_object( $term ) ) { $term = get_term( $term, $taxonomy ); if ( is_wp_error( $term ) ) { return $term; } $use_id = true; } // Don't recurse if we've already identified the term as a child - this indicates a loop. if ( isset( $ancestors[ $term->term_id ] ) ) { continue; } if ( (int) $term->parent === $term_id ) { if ( $use_id ) { $term_list[] = $term->term_id; } else { $term_list[] = $term; } if ( ! isset( $has_children[ $term->term_id ] ) ) { continue; } $ancestors[ $term->term_id ] = 1; $children = _get_term_children( $term->term_id, $terms, $taxonomy, $ancestors ); if ( $children ) { $term_list = array_merge( $term_list, $children ); } } } return $term_list; } /** * Adds count of children to parent count. * * Recalculates term counts by including items from child terms. Assumes all * relevant children are already in the $terms argument. * * @access private * @since 2.3.0 * * @global wpdb $wpdb WordPress database abstraction object. * * @param object[]|WP_Term[] $terms List of term objects (passed by reference). * @param string $taxonomy Term context. */ function _pad_term_counts( &$terms, $taxonomy ) { global $wpdb; // This function only works for hierarchical taxonomies like post categories. if ( ! is_taxonomy_hierarchical( $taxonomy ) ) { return; } $term_hier = _get_term_hierarchy( $taxonomy ); if ( empty( $term_hier ) ) { return; } $term_items = array(); $terms_by_id = array(); $term_ids = array(); foreach ( (array) $terms as $key => $term ) { $terms_by_id[ $term->term_id ] = & $terms[ $key ]; $term_ids[ $term->term_taxonomy_id ] = $term->term_id; } // Get the object and term IDs and stick them in a lookup table. $tax_obj = get_taxonomy( $taxonomy ); $object_types = esc_sql( $tax_obj->object_type ); $results = $wpdb->get_results( "SELECT object_id, term_taxonomy_id FROM $wpdb->term_relationships INNER JOIN $wpdb->posts ON object_id = ID WHERE term_taxonomy_id IN (" . implode( ',', array_keys( $term_ids ) ) . ") AND post_type IN ('" . implode( "', '", $object_types ) . "') AND post_status = 'publish'" ); foreach ( $results as $row ) { $id = $term_ids[ $row->term_taxonomy_id ]; $term_items[ $id ][ $row->object_id ] = isset( $term_items[ $id ][ $row->object_id ] ) ? ++$term_items[ $id ][ $row->object_id ] : 1; } // Touch every ancestor's lookup row for each post in each term. foreach ( $term_ids as $term_id ) { $child = $term_id; $ancestors = array(); while ( ! empty( $terms_by_id[ $child ] ) && $parent = $terms_by_id[ $child ]->parent ) { $ancestors[] = $child; if ( ! empty( $term_items[ $term_id ] ) ) { foreach ( $term_items[ $term_id ] as $item_id => $touches ) { $term_items[ $parent ][ $item_id ] = isset( $term_items[ $parent ][ $item_id ] ) ? ++$term_items[ $parent ][ $item_id ] : 1; } } $child = $parent; if ( in_array( $parent, $ancestors, true ) ) { break; } } } // Transfer the touched cells. foreach ( (array) $term_items as $id => $items ) { if ( isset( $terms_by_id[ $id ] ) ) { $terms_by_id[ $id ]->count = count( $items ); } } } /** * Adds any terms from the given IDs to the cache that do not already exist in cache. * * @since 4.6.0 * @since 6.1.0 This function is no longer marked as "private". * @since 6.3.0 Use wp_lazyload_term_meta() for lazy-loading of term meta. * * @global wpdb $wpdb WordPress database abstraction object. * * @param array $term_ids Array of term IDs. * @param bool $update_meta_cache Optional. Whether to update the meta cache. Default true. */ function _prime_term_caches( $term_ids, $update_meta_cache = true ) { global $wpdb; $non_cached_ids = _get_non_cached_ids( $term_ids, 'terms' ); if ( ! empty( $non_cached_ids ) ) { $fresh_terms = $wpdb->get_results( sprintf( "SELECT t.*, tt.* FROM $wpdb->terms AS t INNER JOIN $wpdb->term_taxonomy AS tt ON t.term_id = tt.term_id WHERE t.term_id IN (%s)", implode( ',', array_map( 'intval', $non_cached_ids ) ) ) ); update_term_cache( $fresh_terms ); } if ( $update_meta_cache ) { wp_lazyload_term_meta( $term_ids ); } } // // Default callbacks. // /** * Updates term count based on object types of the current taxonomy. * * Private function for the default callback for post_tag and category * taxonomies. * * @access private * @since 2.3.0 * * @global wpdb $wpdb WordPress database abstraction object. * * @param int[] $terms List of term taxonomy IDs. * @param WP_Taxonomy $taxonomy Current taxonomy object of terms. */ function _update_post_term_count( $terms, $taxonomy ) { global $wpdb; $object_types = (array) $taxonomy->object_type; foreach ( $object_types as &$object_type ) { list( $object_type ) = explode( ':', $object_type ); } $object_types = array_unique( $object_types ); $check_attachments = array_search( 'attachment', $object_types, true ); if ( false !== $check_attachments ) { unset( $object_types[ $check_attachments ] ); $check_attachments = true; } if ( $object_types ) { $object_types = esc_sql( array_filter( $object_types, 'post_type_exists' ) ); } $post_statuses = array( 'publish' ); /** * Filters the post statuses for updating the term count. * * @since 5.7.0 * * @param string[] $post_statuses List of post statuses to include in the count. Default is 'publish'. * @param WP_Taxonomy $taxonomy Current taxonomy object. */ $post_statuses = esc_sql( apply_filters( 'update_post_term_count_statuses', $post_statuses, $taxonomy ) ); foreach ( (array) $terms as $term ) { $count = 0; // Attachments can be 'inherit' status, we need to base count off the parent's status if so. if ( $check_attachments ) { // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.QuotedDynamicPlaceholderGeneration $count += (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM $wpdb->term_relationships, $wpdb->posts p1 WHERE p1.ID = $wpdb->term_relationships.object_id AND ( post_status IN ('" . implode( "', '", $post_statuses ) . "') OR ( post_status = 'inherit' AND post_parent > 0 AND ( SELECT post_status FROM $wpdb->posts WHERE ID = p1.post_parent ) IN ('" . implode( "', '", $post_statuses ) . "') ) ) AND post_type = 'attachment' AND term_taxonomy_id = %d", $term ) ); } if ( $object_types ) { // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.QuotedDynamicPlaceholderGeneration $count += (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM $wpdb->term_relationships, $wpdb->posts WHERE $wpdb->posts.ID = $wpdb->term_relationships.object_id AND post_status IN ('" . implode( "', '", $post_statuses ) . "') AND post_type IN ('" . implode( "', '", $object_types ) . "') AND term_taxonomy_id = %d", $term ) ); } /** This action is documented in wp-includes/taxonomy.php */ do_action( 'edit_term_taxonomy', $term, $taxonomy->name ); $wpdb->update( $wpdb->term_taxonomy, compact( 'count' ), array( 'term_taxonomy_id' => $term ) ); /** This action is documented in wp-includes/taxonomy.php */ do_action( 'edited_term_taxonomy', $term, $taxonomy->name ); } } /** * Updates term count based on number of objects. * * Default callback for the 'link_category' taxonomy. * * @since 3.3.0 * * @global wpdb $wpdb WordPress database abstraction object. * * @param int[] $terms List of term taxonomy IDs. * @param WP_Taxonomy $taxonomy Current taxonomy object of terms. */ function _update_generic_term_count( $terms, $taxonomy ) { global $wpdb; foreach ( (array) $terms as $term ) { $count = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM $wpdb->term_relationships WHERE term_taxonomy_id = %d", $term ) ); /** This action is documented in wp-includes/taxonomy.php */ do_action( 'edit_term_taxonomy', $term, $taxonomy->name ); $wpdb->update( $wpdb->term_taxonomy, compact( 'count' ), array( 'term_taxonomy_id' => $term ) ); /** This action is documented in wp-includes/taxonomy.php */ do_action( 'edited_term_taxonomy', $term, $taxonomy->name ); } } /** * Creates a new term for a term_taxonomy item that currently shares its term * with another term_taxonomy. * * @ignore * @since 4.2.0 * @since 4.3.0 Introduced `$record` parameter. Also, `$term_id` and * `$term_taxonomy_id` can now accept objects. * * @global wpdb $wpdb WordPress database abstraction object. * * @param int|object $term_id ID of the shared term, or the shared term object. * @param int|object $term_taxonomy_id ID of the term_taxonomy item to receive a new term, or the term_taxonomy object * (corresponding to a row from the term_taxonomy table). * @param bool $record Whether to record data about the split term in the options table. The recording * process has the potential to be resource-intensive, so during batch operations * it can be beneficial to skip inline recording and do it just once, after the * batch is processed. Only set this to `false` if you know what you are doing. * Default: true. * @return int|WP_Error When the current term does not need to be split (or cannot be split on the current * database schema), `$term_id` is returned. When the term is successfully split, the * new term_id is returned. A WP_Error is returned for miscellaneous errors. */ function _split_shared_term( $term_id, $term_taxonomy_id, $record = true ) { global $wpdb; if ( is_object( $term_id ) ) { $shared_term = $term_id; $term_id = (int) $shared_term->term_id; } if ( is_object( $term_taxonomy_id ) ) { $term_taxonomy = $term_taxonomy_id; $term_taxonomy_id = (int) $term_taxonomy->term_taxonomy_id; } // If there are no shared term_taxonomy rows, there's nothing to do here. $shared_tt_count = (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM $wpdb->term_taxonomy tt WHERE tt.term_id = %d AND tt.term_taxonomy_id != %d", $term_id, $term_taxonomy_id ) ); if ( ! $shared_tt_count ) { return $term_id; } /* * Verify that the term_taxonomy_id passed to the function is actually associated with the term_id. * If there's a mismatch, it may mean that the term is already split. Return the actual term_id from the db. */ $check_term_id = (int) $wpdb->get_var( $wpdb->prepare( "SELECT term_id FROM $wpdb->term_taxonomy WHERE term_taxonomy_id = %d", $term_taxonomy_id ) ); if ( $check_term_id !== $term_id ) { return $check_term_id; } // Pull up data about the currently shared slug, which we'll use to populate the new one. if ( empty( $shared_term ) ) { $shared_term = $wpdb->get_row( $wpdb->prepare( "SELECT t.* FROM $wpdb->terms t WHERE t.term_id = %d", $term_id ) ); } $new_term_data = array( 'name' => $shared_term->name, 'slug' => $shared_term->slug, 'term_group' => $shared_term->term_group, ); if ( false === $wpdb->insert( $wpdb->terms, $new_term_data ) ) { return new WP_Error( 'db_insert_error', __( 'Could not split shared term.' ), $wpdb->last_error ); } $new_term_id = (int) $wpdb->insert_id; // Update the existing term_taxonomy to point to the newly created term. $wpdb->update( $wpdb->term_taxonomy, array( 'term_id' => $new_term_id ), array( 'term_taxonomy_id' => $term_taxonomy_id ) ); // Reassign child terms to the new parent. if ( empty( $term_taxonomy ) ) { $term_taxonomy = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM $wpdb->term_taxonomy WHERE term_taxonomy_id = %d", $term_taxonomy_id ) ); } $children_tt_ids = $wpdb->get_col( $wpdb->prepare( "SELECT term_taxonomy_id FROM $wpdb->term_taxonomy WHERE parent = %d AND taxonomy = %s", $term_id, $term_taxonomy->taxonomy ) ); if ( ! empty( $children_tt_ids ) ) { foreach ( $children_tt_ids as $child_tt_id ) { $wpdb->update( $wpdb->term_taxonomy, array( 'parent' => $new_term_id ), array( 'term_taxonomy_id' => $child_tt_id ) ); clean_term_cache( (int) $child_tt_id, '', false ); } } else { // If the term has no children, we must force its taxonomy cache to be rebuilt separately. clean_term_cache( $new_term_id, $term_taxonomy->taxonomy, false ); } clean_term_cache( $term_id, $term_taxonomy->taxonomy, false ); /* * Taxonomy cache clearing is delayed to avoid race conditions that may occur when * regenerating the taxonomy's hierarchy tree. */ $taxonomies_to_clean = array( $term_taxonomy->taxonomy ); // Clean the cache for term taxonomies formerly shared with the current term. $shared_term_taxonomies = $wpdb->get_col( $wpdb->prepare( "SELECT taxonomy FROM $wpdb->term_taxonomy WHERE term_id = %d", $term_id ) ); $taxonomies_to_clean = array_merge( $taxonomies_to_clean, $shared_term_taxonomies ); foreach ( $taxonomies_to_clean as $taxonomy_to_clean ) { clean_taxonomy_cache( $taxonomy_to_clean ); } // Keep a record of term_ids that have been split, keyed by old term_id. See wp_get_split_term(). if ( $record ) { $split_term_data = get_option( '_split_terms', array() ); if ( ! isset( $split_term_data[ $term_id ] ) ) { $split_term_data[ $term_id ] = array(); } $split_term_data[ $term_id ][ $term_taxonomy->taxonomy ] = $new_term_id; update_option( '_split_terms', $split_term_data ); } // If we've just split the final shared term, set the "finished" flag. $shared_terms_exist = $wpdb->get_results( "SELECT tt.term_id, t.*, count(*) as term_tt_count FROM {$wpdb->term_taxonomy} tt LEFT JOIN {$wpdb->terms} t ON t.term_id = tt.term_id GROUP BY t.term_id HAVING term_tt_count > 1 LIMIT 1" ); if ( ! $shared_terms_exist ) { update_option( 'finished_splitting_shared_terms', true ); } /** * Fires after a previously shared taxonomy term is split into two separate terms. * * @since 4.2.0 * * @param int $term_id ID of the formerly shared term. * @param int $new_term_id ID of the new term created for the $term_taxonomy_id. * @param int $term_taxonomy_id ID for the term_taxonomy row affected by the split. * @param string $taxonomy Taxonomy for the split term. */ do_action( 'split_shared_term', $term_id, $new_term_id, $term_taxonomy_id, $term_taxonomy->taxonomy ); return $new_term_id; } /** * Splits a batch of shared taxonomy terms. * * @since 4.3.0 * * @global wpdb $wpdb WordPress database abstraction object. */ function _wp_batch_split_terms() { global $wpdb; $lock_name = 'term_split.lock'; // Try to lock. $lock_result = $wpdb->query( $wpdb->prepare( "INSERT IGNORE INTO `$wpdb->options` ( `option_name`, `option_value`, `autoload` ) VALUES (%s, %s, 'no') /* LOCK */", $lock_name, time() ) ); if ( ! $lock_result ) { $lock_result = get_option( $lock_name ); // Bail if we were unable to create a lock, or if the existing lock is still valid. if ( ! $lock_result || ( $lock_result > ( time() - HOUR_IN_SECONDS ) ) ) { wp_schedule_single_event( time() + ( 5 * MINUTE_IN_SECONDS ), 'wp_split_shared_term_batch' ); return; } } // Update the lock, as by this point we've definitely got a lock, just need to fire the actions. update_option( $lock_name, time() ); // Get a list of shared terms (those with more than one associated row in term_taxonomy). $shared_terms = $wpdb->get_results( "SELECT tt.term_id, t.*, count(*) as term_tt_count FROM {$wpdb->term_taxonomy} tt LEFT JOIN {$wpdb->terms} t ON t.term_id = tt.term_id GROUP BY t.term_id HAVING term_tt_count > 1 LIMIT 10" ); // No more terms, we're done here. if ( ! $shared_terms ) { update_option( 'finished_splitting_shared_terms', true ); delete_option( $lock_name ); return; } // Shared terms found? We'll need to run this script again. wp_schedule_single_event( time() + ( 2 * MINUTE_IN_SECONDS ), 'wp_split_shared_term_batch' ); // Rekey shared term array for faster lookups. $_shared_terms = array(); foreach ( $shared_terms as $shared_term ) { $term_id = (int) $shared_term->term_id; $_shared_terms[ $term_id ] = $shared_term; } $shared_terms = $_shared_terms; // Get term taxonomy data for all shared terms. $shared_term_ids = implode( ',', array_keys( $shared_terms ) ); $shared_tts = $wpdb->get_results( "SELECT * FROM {$wpdb->term_taxonomy} WHERE `term_id` IN ({$shared_term_ids})" ); // Split term data recording is slow, so we do it just once, outside the loop. $split_term_data = get_option( '_split_terms', array() ); $skipped_first_term = array(); $taxonomies = array(); foreach ( $shared_tts as $shared_tt ) { $term_id = (int) $shared_tt->term_id; // Don't split the first tt belonging to a given term_id. if ( ! isset( $skipped_first_term[ $term_id ] ) ) { $skipped_first_term[ $term_id ] = 1; continue; } if ( ! isset( $split_term_data[ $term_id ] ) ) { $split_term_data[ $term_id ] = array(); } // Keep track of taxonomies whose hierarchies need flushing. if ( ! isset( $taxonomies[ $shared_tt->taxonomy ] ) ) { $taxonomies[ $shared_tt->taxonomy ] = 1; } // Split the term. $split_term_data[ $term_id ][ $shared_tt->taxonomy ] = _split_shared_term( $shared_terms[ $term_id ], $shared_tt, false ); } // Rebuild the cached hierarchy for each affected taxonomy. foreach ( array_keys( $taxonomies ) as $tax ) { delete_option( "{$tax}_children" ); _get_term_hierarchy( $tax ); } update_option( '_split_terms', $split_term_data ); delete_option( $lock_name ); } /** * In order to avoid the _wp_batch_split_terms() job being accidentally removed, * checks that it's still scheduled while we haven't finished splitting terms. * * @ignore * @since 4.3.0 */ function _wp_check_for_scheduled_split_terms() { if ( ! get_option( 'finished_splitting_shared_terms' ) && ! wp_next_scheduled( 'wp_split_shared_term_batch' ) ) { wp_schedule_single_event( time() + MINUTE_IN_SECONDS, 'wp_split_shared_term_batch' ); } } /** * Checks default categories when a term gets split to see if any of them need to be updated. * * @ignore * @since 4.2.0 * * @param int $term_id ID of the formerly shared term. * @param int $new_term_id ID of the new term created for the $term_taxonomy_id. * @param int $term_taxonomy_id ID for the term_taxonomy row affected by the split. * @param string $taxonomy Taxonomy for the split term. */ function _wp_check_split_default_terms( $term_id, $new_term_id, $term_taxonomy_id, $taxonomy ) { if ( 'category' !== $taxonomy ) { return; } foreach ( array( 'default_category', 'default_link_category', 'default_email_category' ) as $option ) { if ( (int) get_option( $option, -1 ) === $term_id ) { update_option( $option, $new_term_id ); } } } /** * Checks menu items when a term gets split to see if any of them need to be updated. * * @ignore * @since 4.2.0 * * @global wpdb $wpdb WordPress database abstraction object. * * @param int $term_id ID of the formerly shared term. * @param int $new_term_id ID of the new term created for the $term_taxonomy_id. * @param int $term_taxonomy_id ID for the term_taxonomy row affected by the split. * @param string $taxonomy Taxonomy for the split term. */ function _wp_check_split_terms_in_menus( $term_id, $new_term_id, $term_taxonomy_id, $taxonomy ) { global $wpdb; $post_ids = $wpdb->get_col( $wpdb->prepare( "SELECT m1.post_id FROM {$wpdb->postmeta} AS m1 INNER JOIN {$wpdb->postmeta} AS m2 ON ( m2.post_id = m1.post_id ) INNER JOIN {$wpdb->postmeta} AS m3 ON ( m3.post_id = m1.post_id ) WHERE ( m1.meta_key = '_menu_item_type' AND m1.meta_value = 'taxonomy' ) AND ( m2.meta_key = '_menu_item_object' AND m2.meta_value = %s ) AND ( m3.meta_key = '_menu_item_object_id' AND m3.meta_value = %d )", $taxonomy, $term_id ) ); if ( $post_ids ) { foreach ( $post_ids as $post_id ) { update_post_meta( $post_id, '_menu_item_object_id', $new_term_id, $term_id ); } } } /** * If the term being split is a nav_menu, changes associations. * * @ignore * @since 4.3.0 * * @param int $term_id ID of the formerly shared term. * @param int $new_term_id ID of the new term created for the $term_taxonomy_id. * @param int $term_taxonomy_id ID for the term_taxonomy row affected by the split. * @param string $taxonomy Taxonomy for the split term. */ function _wp_check_split_nav_menu_terms( $term_id, $new_term_id, $term_taxonomy_id, $taxonomy ) { if ( 'nav_menu' !== $taxonomy ) { return; } // Update menu locations. $locations = get_nav_menu_locations(); foreach ( $locations as $location => $menu_id ) { if ( $term_id === $menu_id ) { $locations[ $location ] = $new_term_id; } } set_theme_mod( 'nav_menu_locations', $locations ); } /** * Gets data about terms that previously shared a single term_id, but have since been split. * * @since 4.2.0 * * @param int $old_term_id Term ID. This is the old, pre-split term ID. * @return array Array of new term IDs, keyed by taxonomy. */ function wp_get_split_terms( $old_term_id ) { $split_terms = get_option( '_split_terms', array() ); $terms = array(); if ( isset( $split_terms[ $old_term_id ] ) ) { $terms = $split_terms[ $old_term_id ]; } return $terms; } /** * Gets the new term ID corresponding to a previously split term. * * @since 4.2.0 * * @param int $old_term_id Term ID. This is the old, pre-split term ID. * @param string $taxonomy Taxonomy that the term belongs to. * @return int|false If a previously split term is found corresponding to the old term_id and taxonomy, * the new term_id will be returned. If no previously split term is found matching * the parameters, returns false. */ function wp_get_split_term( $old_term_id, $taxonomy ) { $split_terms = wp_get_split_terms( $old_term_id ); $term_id = false; if ( isset( $split_terms[ $taxonomy ] ) ) { $term_id = (int) $split_terms[ $taxonomy ]; } return $term_id; } /** * Determines whether a term is shared between multiple taxonomies. * * Shared taxonomy terms began to be split in 4.3, but failed cron tasks or * other delays in upgrade routines may cause shared terms to remain. * * @since 4.4.0 * * @global wpdb $wpdb WordPress database abstraction object. * * @param int $term_id Term ID. * @return bool Returns false if a term is not shared between multiple taxonomies or * if splitting shared taxonomy terms is finished. */ function wp_term_is_shared( $term_id ) { global $wpdb; if ( get_option( 'finished_splitting_shared_terms' ) ) { return false; } $tt_count = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM $wpdb->term_taxonomy WHERE term_id = %d", $term_id ) ); return $tt_count > 1; } /** * Generates a permalink for a taxonomy term archive. * * @since 2.5.0 * * @global WP_Rewrite $wp_rewrite WordPress rewrite component. * * @param WP_Term|int|string $term The term object, ID, or slug whose link will be retrieved. * @param string $taxonomy Optional. Taxonomy. Default empty. * @return string|WP_Error URL of the taxonomy term archive on success, WP_Error if term does not exist. */ function get_term_link( $term, $taxonomy = '' ) { global $wp_rewrite; if ( ! is_object( $term ) ) { if ( is_int( $term ) ) { $term = get_term( $term, $taxonomy ); } else { $term = get_term_by( 'slug', $term, $taxonomy ); } } if ( ! is_object( $term ) ) { $term = new WP_Error( 'invalid_term', __( 'Empty Term.' ) ); } if ( is_wp_error( $term ) ) { return $term; } $taxonomy = $term->taxonomy; $termlink = $wp_rewrite->get_extra_permastruct( $taxonomy ); /** * Filters the permalink structure for a term before token replacement occurs. * * @since 4.9.0 * * @param string $termlink The permalink structure for the term's taxonomy. * @param WP_Term $term The term object. */ $termlink = apply_filters( 'pre_term_link', $termlink, $term ); $slug = $term->slug; $t = get_taxonomy( $taxonomy ); if ( empty( $termlink ) ) { if ( 'category' === $taxonomy ) { $termlink = '?cat=' . $term->term_id; } elseif ( $t->query_var ) { $termlink = "?$t->query_var=$slug"; } else { $termlink = "?taxonomy=$taxonomy&term=$slug"; } $termlink = home_url( $termlink ); } else { if ( ! empty( $t->rewrite['hierarchical'] ) ) { $hierarchical_slugs = array(); $ancestors = get_ancestors( $term->term_id, $taxonomy, 'taxonomy' ); foreach ( (array) $ancestors as $ancestor ) { $ancestor_term = get_term( $ancestor, $taxonomy ); $hierarchical_slugs[] = $ancestor_term->slug; } $hierarchical_slugs = array_reverse( $hierarchical_slugs ); $hierarchical_slugs[] = $slug; $termlink = str_replace( "%$taxonomy%", implode( '/', $hierarchical_slugs ), $termlink ); } else { $termlink = str_replace( "%$taxonomy%", $slug, $termlink ); } $termlink = home_url( user_trailingslashit( $termlink, 'category' ) ); } // Back compat filters. if ( 'post_tag' === $taxonomy ) { /** * Filters the tag link. * * @since 2.3.0 * @since 2.5.0 Deprecated in favor of {@see 'term_link'} filter. * @since 5.4.1 Restored (un-deprecated). * * @param string $termlink Tag link URL. * @param int $term_id Term ID. */ $termlink = apply_filters( 'tag_link', $termlink, $term->term_id ); } elseif ( 'category' === $taxonomy ) { /** * Filters the category link. * * @since 1.5.0 * @since 2.5.0 Deprecated in favor of {@see 'term_link'} filter. * @since 5.4.1 Restored (un-deprecated). * * @param string $termlink Category link URL. * @param int $term_id Term ID. */ $termlink = apply_filters( 'category_link', $termlink, $term->term_id ); } /** * Filters the term link. * * @since 2.5.0 * * @param string $termlink Term link URL. * @param WP_Term $term Term object. * @param string $taxonomy Taxonomy slug. */ return apply_filters( 'term_link', $termlink, $term, $taxonomy ); } /** * Displays the taxonomies of a post with available options. * * This function can be used within the loop to display the taxonomies for a * post without specifying the Post ID. You can also use it outside the Loop to * display the taxonomies for a specific post. * * @since 2.5.0 * * @param array $args { * Arguments about which post to use and how to format the output. Shares all of the arguments * supported by get_the_taxonomies(), in addition to the following. * * @type int|WP_Post $post Post ID or object to get taxonomies of. Default current post. * @type string $before Displays before the taxonomies. Default empty string. * @type string $sep Separates each taxonomy. Default is a space. * @type string $after Displays after the taxonomies. Default empty string. * } */ function the_taxonomies( $args = array() ) { $defaults = array( 'post' => 0, 'before' => '', 'sep' => ' ', 'after' => '', ); $parsed_args = wp_parse_args( $args, $defaults ); echo $parsed_args['before'] . implode( $parsed_args['sep'], get_the_taxonomies( $parsed_args['post'], $parsed_args ) ) . $parsed_args['after']; } /** * Retrieves all taxonomies associated with a post. * * This function can be used within the loop. It will also return an array of * the taxonomies with links to the taxonomy and name. * * @since 2.5.0 * * @param int|WP_Post $post Optional. Post ID or WP_Post object. Default is global $post. * @param array $args { * Optional. Arguments about how to format the list of taxonomies. Default empty array. * * @type string $template Template for displaying a taxonomy label and list of terms. * Default is "Label: Terms." * @type string $term_template Template for displaying a single term in the list. Default is the term name * linked to its archive. * } * @return string[] List of taxonomies. */ function get_the_taxonomies( $post = 0, $args = array() ) { $post = get_post( $post ); $args = wp_parse_args( $args, array( /* translators: %s: Taxonomy label, %l: List of terms formatted as per $term_template. */ 'template' => __( '%s: %l.' ), 'term_template' => '%2$s', ) ); $taxonomies = array(); if ( ! $post ) { return $taxonomies; } foreach ( get_object_taxonomies( $post ) as $taxonomy ) { $t = (array) get_taxonomy( $taxonomy ); if ( empty( $t['label'] ) ) { $t['label'] = $taxonomy; } if ( empty( $t['args'] ) ) { $t['args'] = array(); } if ( empty( $t['template'] ) ) { $t['template'] = $args['template']; } if ( empty( $t['term_template'] ) ) { $t['term_template'] = $args['term_template']; } $terms = get_object_term_cache( $post->ID, $taxonomy ); if ( false === $terms ) { $terms = wp_get_object_terms( $post->ID, $taxonomy, $t['args'] ); } $links = array(); foreach ( $terms as $term ) { $links[] = wp_sprintf( $t['term_template'], esc_attr( get_term_link( $term ) ), $term->name ); } if ( $links ) { $taxonomies[ $taxonomy ] = wp_sprintf( $t['template'], $t['label'], $links, $terms ); } } return $taxonomies; } /** * Retrieves all taxonomy names for the given post. * * @since 2.5.0 * * @param int|WP_Post $post Optional. Post ID or WP_Post object. Default is global $post. * @return string[] An array of all taxonomy names for the given post. */ function get_post_taxonomies( $post = 0 ) { $post = get_post( $post ); return get_object_taxonomies( $post ); } /** * Determines if the given object is associated with any of the given terms. * * The given terms are checked against the object's terms' term_ids, names and slugs. * Terms given as integers will only be checked against the object's terms' term_ids. * If no terms are given, determines if object is associated with any terms in the given taxonomy. * * @since 2.7.0 * * @param int $object_id ID of the object (post ID, link ID, ...). * @param string $taxonomy Single taxonomy name. * @param int|string|int[]|string[] $terms Optional. Term ID, name, slug, or array of such * to check against. Default null. * @return bool|WP_Error WP_Error on input error. */ function is_object_in_term( $object_id, $taxonomy, $terms = null ) { $object_id = (int) $object_id; if ( ! $object_id ) { return new WP_Error( 'invalid_object', __( 'Invalid object ID.' ) ); } $object_terms = get_object_term_cache( $object_id, $taxonomy ); if ( false === $object_terms ) { $object_terms = wp_get_object_terms( $object_id, $taxonomy, array( 'update_term_meta_cache' => false ) ); if ( is_wp_error( $object_terms ) ) { return $object_terms; } wp_cache_set( $object_id, wp_list_pluck( $object_terms, 'term_id' ), "{$taxonomy}_relationships" ); } if ( is_wp_error( $object_terms ) ) { return $object_terms; } if ( empty( $object_terms ) ) { return false; } if ( empty( $terms ) ) { return ( ! empty( $object_terms ) ); } $terms = (array) $terms; $ints = array_filter( $terms, 'is_int' ); if ( $ints ) { $strs = array_diff( $terms, $ints ); } else { $strs =& $terms; } foreach ( $object_terms as $object_term ) { // If term is an int, check against term_ids only. if ( $ints && in_array( $object_term->term_id, $ints, true ) ) { return true; } if ( $strs ) { // Only check numeric strings against term_id, to avoid false matches due to type juggling. $numeric_strs = array_map( 'intval', array_filter( $strs, 'is_numeric' ) ); if ( in_array( $object_term->term_id, $numeric_strs, true ) ) { return true; } if ( in_array( $object_term->name, $strs, true ) ) { return true; } if ( in_array( $object_term->slug, $strs, true ) ) { return true; } } } return false; } /** * Determines if the given object type is associated with the given taxonomy. * * @since 3.0.0 * * @param string $object_type Object type string. * @param string $taxonomy Single taxonomy name. * @return bool True if object is associated with the taxonomy, otherwise false. */ function is_object_in_taxonomy( $object_type, $taxonomy ) { $taxonomies = get_object_taxonomies( $object_type ); if ( empty( $taxonomies ) ) { return false; } return in_array( $taxonomy, $taxonomies, true ); } /** * Gets an array of ancestor IDs for a given object. * * @since 3.1.0 * @since 4.1.0 Introduced the `$resource_type` argument. * * @param int $object_id Optional. The ID of the object. Default 0. * @param string $object_type Optional. The type of object for which we'll be retrieving * ancestors. Accepts a post type or a taxonomy name. Default empty. * @param string $resource_type Optional. Type of resource $object_type is. Accepts 'post_type' * or 'taxonomy'. Default empty. * @return int[] An array of IDs of ancestors from lowest to highest in the hierarchy. */ function get_ancestors( $object_id = 0, $object_type = '', $resource_type = '' ) { $object_id = (int) $object_id; $ancestors = array(); if ( empty( $object_id ) ) { /** This filter is documented in wp-includes/taxonomy.php */ return apply_filters( 'get_ancestors', $ancestors, $object_id, $object_type, $resource_type ); } if ( ! $resource_type ) { if ( is_taxonomy_hierarchical( $object_type ) ) { $resource_type = 'taxonomy'; } elseif ( post_type_exists( $object_type ) ) { $resource_type = 'post_type'; } } if ( 'taxonomy' === $resource_type ) { $term = get_term( $object_id, $object_type ); while ( ! is_wp_error( $term ) && ! empty( $term->parent ) && ! in_array( $term->parent, $ancestors, true ) ) { $ancestors[] = (int) $term->parent; $term = get_term( $term->parent, $object_type ); } } elseif ( 'post_type' === $resource_type ) { $ancestors = get_post_ancestors( $object_id ); } /** * Filters a given object's ancestors. * * @since 3.1.0 * @since 4.1.1 Introduced the `$resource_type` parameter. * * @param int[] $ancestors An array of IDs of object ancestors. * @param int $object_id Object ID. * @param string $object_type Type of object. * @param string $resource_type Type of resource $object_type is. */ return apply_filters( 'get_ancestors', $ancestors, $object_id, $object_type, $resource_type ); } /** * Returns the term's parent's term ID. * * @since 3.1.0 * * @param int $term_id Term ID. * @param string $taxonomy Taxonomy name. * @return int|false Parent term ID on success, false on failure. */ function wp_get_term_taxonomy_parent_id( $term_id, $taxonomy ) { $term = get_term( $term_id, $taxonomy ); if ( ! $term || is_wp_error( $term ) ) { return false; } return (int) $term->parent; } /** * Checks the given subset of the term hierarchy for hierarchy loops. * Prevents loops from forming and breaks those that it finds. * * Attached to the {@see 'wp_update_term_parent'} filter. * * @since 3.1.0 * * @param int $parent_term `term_id` of the parent for the term we're checking. * @param int $term_id The term we're checking. * @param string $taxonomy The taxonomy of the term we're checking. * @return int The new parent for the term. */ function wp_check_term_hierarchy_for_loops( $parent_term, $term_id, $taxonomy ) { // Nothing fancy here - bail. if ( ! $parent_term ) { return 0; } // Can't be its own parent. if ( $parent_term === $term_id ) { return 0; } // Now look for larger loops. $loop = wp_find_hierarchy_loop( 'wp_get_term_taxonomy_parent_id', $term_id, $parent_term, array( $taxonomy ) ); if ( ! $loop ) { return $parent_term; // No loop. } // Setting $parent_term to the given value causes a loop. if ( isset( $loop[ $term_id ] ) ) { return 0; } // There's a loop, but it doesn't contain $term_id. Break the loop. foreach ( array_keys( $loop ) as $loop_member ) { wp_update_term( $loop_member, $taxonomy, array( 'parent' => 0 ) ); } return $parent_term; } /** * Determines whether a taxonomy is considered "viewable". * * @since 5.1.0 * * @param string|WP_Taxonomy $taxonomy Taxonomy name or object. * @return bool Whether the taxonomy should be considered viewable. */ function is_taxonomy_viewable( $taxonomy ) { if ( is_scalar( $taxonomy ) ) { $taxonomy = get_taxonomy( $taxonomy ); if ( ! $taxonomy ) { return false; } } return $taxonomy->publicly_queryable; } /** * Determines whether a term is publicly viewable. * * A term is considered publicly viewable if its taxonomy is viewable. * * @since 6.1.0 * * @param int|WP_Term $term Term ID or term object. * @return bool Whether the term is publicly viewable. */ function is_term_publicly_viewable( $term ) { $term = get_term( $term ); if ( ! $term ) { return false; } return is_taxonomy_viewable( $term->taxonomy ); } /** * Sets the last changed time for the 'terms' cache group. * * @since 5.0.0 */ function wp_cache_set_terms_last_changed() { wp_cache_set_last_changed( 'terms' ); } /** * Aborts calls to term meta if it is not supported. * * @since 5.0.0 * * @param mixed $check Skip-value for whether to proceed term meta function execution. * @return mixed Original value of $check, or false if term meta is not supported. */ function wp_check_term_meta_support_prefilter( $check ) { if ( get_option( 'db_version' ) < 34370 ) { return false; } return $check; }