/** * WP_Theme Class * * @package WordPress * @subpackage Theme * @since 3.4.0 */ #[AllowDynamicProperties] final class WP_Theme implements ArrayAccess { /** * Whether the theme has been marked as updateable. * * @since 4.4.0 * @var bool * * @see WP_MS_Themes_List_Table */ public $update = false; /** * Headers for style.css files. * * @since 3.4.0 * @since 5.4.0 Added `Requires at least` and `Requires PHP` headers. * @since 6.1.0 Added `Update URI` header. * @var string[] */ private static $file_headers = array( 'Name' => 'Theme Name', 'ThemeURI' => 'Theme URI', 'Description' => 'Description', 'Author' => 'Author', 'AuthorURI' => 'Author URI', 'Version' => 'Version', 'Template' => 'Template', 'Status' => 'Status', 'Tags' => 'Tags', 'TextDomain' => 'Text Domain', 'DomainPath' => 'Domain Path', 'RequiresWP' => 'Requires at least', 'RequiresPHP' => 'Requires PHP', 'UpdateURI' => 'Update URI', ); /** * Default themes. * * @since 3.4.0 * @since 3.5.0 Added the Twenty Twelve theme. * @since 3.6.0 Added the Twenty Thirteen theme. * @since 3.8.0 Added the Twenty Fourteen theme. * @since 4.1.0 Added the Twenty Fifteen theme. * @since 4.4.0 Added the Twenty Sixteen theme. * @since 4.7.0 Added the Twenty Seventeen theme. * @since 5.0.0 Added the Twenty Nineteen theme. * @since 5.3.0 Added the Twenty Twenty theme. * @since 5.6.0 Added the Twenty Twenty-One theme. * @since 5.9.0 Added the Twenty Twenty-Two theme. * @since 6.1.0 Added the Twenty Twenty-Three theme. * @var string[] */ private static $default_themes = array( 'classic' => 'WordPress Classic', 'default' => 'WordPress Default', 'twentyten' => 'Twenty Ten', 'twentyeleven' => 'Twenty Eleven', 'twentytwelve' => 'Twenty Twelve', 'twentythirteen' => 'Twenty Thirteen', 'twentyfourteen' => 'Twenty Fourteen', 'twentyfifteen' => 'Twenty Fifteen', 'twentysixteen' => 'Twenty Sixteen', 'twentyseventeen' => 'Twenty Seventeen', 'twentynineteen' => 'Twenty Nineteen', 'twentytwenty' => 'Twenty Twenty', 'twentytwentyone' => 'Twenty Twenty-One', 'twentytwentytwo' => 'Twenty Twenty-Two', 'twentytwentythree' => 'Twenty Twenty-Three', ); /** * Renamed theme tags. * * @since 3.8.0 * @var string[] */ private static $tag_map = array( 'fixed-width' => 'fixed-layout', 'flexible-width' => 'fluid-layout', ); /** * Absolute path to the theme root, usually wp-content/themes * * @since 3.4.0 * @var string */ private $theme_root; /** * Header data from the theme's style.css file. * * @since 3.4.0 * @var array */ private $headers = array(); /** * Header data from the theme's style.css file after being sanitized. * * @since 3.4.0 * @var array */ private $headers_sanitized; /** * Is this theme a block theme. * * @since 6.2.0 * @var bool */ private $block_theme; /** * Header name from the theme's style.css after being translated. * * Cached due to sorting functions running over the translated name. * * @since 3.4.0 * @var string */ private $name_translated; /** * Errors encountered when initializing the theme. * * @since 3.4.0 * @var WP_Error */ private $errors; /** * The directory name of the theme's files, inside the theme root. * * In the case of a child theme, this is directory name of the child theme. * Otherwise, 'stylesheet' is the same as 'template'. * * @since 3.4.0 * @var string */ private $stylesheet; /** * The directory name of the theme's files, inside the theme root. * * In the case of a child theme, this is the directory name of the parent theme. * Otherwise, 'template' is the same as 'stylesheet'. * * @since 3.4.0 * @var string */ private $template; /** * A reference to the parent theme, in the case of a child theme. * * @since 3.4.0 * @var WP_Theme */ private $parent; /** * URL to the theme root, usually an absolute URL to wp-content/themes * * @since 3.4.0 * @var string */ private $theme_root_uri; /** * Flag for whether the theme's textdomain is loaded. * * @since 3.4.0 * @var bool */ private $textdomain_loaded; /** * Stores an md5 hash of the theme root, to function as the cache key. * * @since 3.4.0 * @var string */ private $cache_hash; /** * Flag for whether the themes cache bucket should be persistently cached. * * Default is false. Can be set with the {@see 'wp_cache_themes_persistently'} filter. * * @since 3.4.0 * @var bool */ private static $persistently_cache; /** * Expiration time for the themes cache bucket. * * By default the bucket is not cached, so this value is useless. * * @since 3.4.0 * @var bool */ private static $cache_expiration = 1800; /** * Constructor for WP_Theme. * * @since 3.4.0 * * @global array $wp_theme_directories * * @param string $theme_dir Directory of the theme within the theme_root. * @param string $theme_root Theme root. * @param WP_Theme|null $_child If this theme is a parent theme, the child may be passed for validation purposes. */ public function __construct( $theme_dir, $theme_root, $_child = null ) { global $wp_theme_directories; // Initialize caching on first run. if ( ! isset( self::$persistently_cache ) ) { /** This action is documented in wp-includes/theme.php */ self::$persistently_cache = apply_filters( 'wp_cache_themes_persistently', false, 'WP_Theme' ); if ( self::$persistently_cache ) { wp_cache_add_global_groups( 'themes' ); if ( is_int( self::$persistently_cache ) ) { self::$cache_expiration = self::$persistently_cache; } } else { wp_cache_add_non_persistent_groups( 'themes' ); } } // Handle a numeric theme directory as a string. $theme_dir = (string) $theme_dir; $this->theme_root = $theme_root; $this->stylesheet = $theme_dir; // Correct a situation where the theme is 'some-directory/some-theme' but 'some-directory' was passed in as part of the theme root instead. if ( ! in_array( $theme_root, (array) $wp_theme_directories, true ) && in_array( dirname( $theme_root ), (array) $wp_theme_directories, true ) ) { $this->stylesheet = basename( $this->theme_root ) . '/' . $this->stylesheet; $this->theme_root = dirname( $theme_root ); } $this->cache_hash = md5( $this->theme_root . '/' . $this->stylesheet ); $theme_file = $this->stylesheet . '/style.css'; $cache = $this->cache_get( 'theme' ); if ( is_array( $cache ) ) { foreach ( array( 'block_theme', 'errors', 'headers', 'template' ) as $key ) { if ( isset( $cache[ $key ] ) ) { $this->$key = $cache[ $key ]; } } if ( $this->errors ) { return; } if ( isset( $cache['theme_root_template'] ) ) { $theme_root_template = $cache['theme_root_template']; } } elseif ( ! file_exists( $this->theme_root . '/' . $theme_file ) ) { $this->headers['Name'] = $this->stylesheet; if ( ! file_exists( $this->theme_root . '/' . $this->stylesheet ) ) { $this->errors = new WP_Error( 'theme_not_found', sprintf( /* translators: %s: Theme directory name. */ __( 'The theme directory "%s" does not exist.' ), esc_html( $this->stylesheet ) ) ); } else { $this->errors = new WP_Error( 'theme_no_stylesheet', __( 'Stylesheet is missing.' ) ); } $this->template = $this->stylesheet; $this->block_theme = false; $this->cache_add( 'theme', array( 'block_theme' => $this->block_theme, 'headers' => $this->headers, 'errors' => $this->errors, 'stylesheet' => $this->stylesheet, 'template' => $this->template, ) ); if ( ! file_exists( $this->theme_root ) ) { // Don't cache this one. $this->errors->add( 'theme_root_missing', __( 'Error: The themes directory is either empty or does not exist. Please check your installation.' ) ); } return; } elseif ( ! is_readable( $this->theme_root . '/' . $theme_file ) ) { $this->headers['Name'] = $this->stylesheet; $this->errors = new WP_Error( 'theme_stylesheet_not_readable', __( 'Stylesheet is not readable.' ) ); $this->template = $this->stylesheet; $this->block_theme = false; $this->cache_add( 'theme', array( 'block_theme' => $this->block_theme, 'headers' => $this->headers, 'errors' => $this->errors, 'stylesheet' => $this->stylesheet, 'template' => $this->template, ) ); return; } else { $this->headers = get_file_data( $this->theme_root . '/' . $theme_file, self::$file_headers, 'theme' ); // Default themes always trump their pretenders. // Properly identify default themes that are inside a directory within wp-content/themes. $default_theme_slug = array_search( $this->headers['Name'], self::$default_themes, true ); if ( $default_theme_slug ) { if ( basename( $this->stylesheet ) != $default_theme_slug ) { $this->headers['Name'] .= '/' . $this->stylesheet; } } } if ( ! $this->template && $this->stylesheet === $this->headers['Template'] ) { $this->errors = new WP_Error( 'theme_child_invalid', sprintf( /* translators: %s: Template. */ __( 'The theme defines itself as its parent theme. Please check the %s header.' ), 'Template' ) ); $this->cache_add( 'theme', array( 'block_theme' => $this->is_block_theme(), 'headers' => $this->headers, 'errors' => $this->errors, 'stylesheet' => $this->stylesheet, ) ); return; } // (If template is set from cache [and there are no errors], we know it's good.) if ( ! $this->template ) { $this->template = $this->headers['Template']; } if ( ! $this->template ) { $this->template = $this->stylesheet; $theme_path = $this->theme_root . '/' . $this->stylesheet; if ( ! file_exists( $theme_path . '/templates/index.html' ) && ! file_exists( $theme_path . '/block-templates/index.html' ) // Deprecated path support since 5.9.0. && ! file_exists( $theme_path . '/index.php' ) ) { $error_message = sprintf( /* translators: 1: templates/index.html, 2: index.php, 3: Documentation URL, 4: Template, 5: style.css */ __( 'Template is missing. Standalone themes need to have a %1$s or %2$s template file. Child themes need to have a %4$s header in the %5$s stylesheet.' ), 'templates/index.html', 'index.php', __( 'https://developer.wordpress.org/themes/advanced-topics/child-themes/' ), 'Template', 'style.css' ); $this->errors = new WP_Error( 'theme_no_index', $error_message ); $this->cache_add( 'theme', array( 'block_theme' => $this->is_block_theme(), 'headers' => $this->headers, 'errors' => $this->errors, 'stylesheet' => $this->stylesheet, 'template' => $this->template, ) ); return; } } // If we got our data from cache, we can assume that 'template' is pointing to the right place. if ( ! is_array( $cache ) && $this->template != $this->stylesheet && ! file_exists( $this->theme_root . '/' . $this->template . '/index.php' ) ) { // If we're in a directory of themes inside /themes, look for the parent nearby. // wp-content/themes/directory-of-themes/* $parent_dir = dirname( $this->stylesheet ); $directories = search_theme_directories(); if ( '.' !== $parent_dir && file_exists( $this->theme_root . '/' . $parent_dir . '/' . $this->template . '/index.php' ) ) { $this->template = $parent_dir . '/' . $this->template; } elseif ( $directories && isset( $directories[ $this->template ] ) ) { // Look for the template in the search_theme_directories() results, in case it is in another theme root. // We don't look into directories of themes, just the theme root. $theme_root_template = $directories[ $this->template ]['theme_root']; } else { // Parent theme is missing. $this->errors = new WP_Error( 'theme_no_parent', sprintf( /* translators: %s: Theme directory name. */ __( 'The parent theme is missing. Please install the "%s" parent theme.' ), esc_html( $this->template ) ) ); $this->cache_add( 'theme', array( 'block_theme' => $this->is_block_theme(), 'headers' => $this->headers, 'errors' => $this->errors, 'stylesheet' => $this->stylesheet, 'template' => $this->template, ) ); $this->parent = new WP_Theme( $this->template, $this->theme_root, $this ); return; } } // Set the parent, if we're a child theme. if ( $this->template != $this->stylesheet ) { // If we are a parent, then there is a problem. Only two generations allowed! Cancel things out. if ( $_child instanceof WP_Theme && $_child->template == $this->stylesheet ) { $_child->parent = null; $_child->errors = new WP_Error( 'theme_parent_invalid', sprintf( /* translators: %s: Theme directory name. */ __( 'The "%s" theme is not a valid parent theme.' ), esc_html( $_child->template ) ) ); $_child->cache_add( 'theme', array( 'block_theme' => $_child->is_block_theme(), 'headers' => $_child->headers, 'errors' => $_child->errors, 'stylesheet' => $_child->stylesheet, 'template' => $_child->template, ) ); // The two themes actually reference each other with the Template header. if ( $_child->stylesheet == $this->template ) { $this->errors = new WP_Error( 'theme_parent_invalid', sprintf( /* translators: %s: Theme directory name. */ __( 'The "%s" theme is not a valid parent theme.' ), esc_html( $this->template ) ) ); $this->cache_add( 'theme', array( 'block_theme' => $this->is_block_theme(), 'headers' => $this->headers, 'errors' => $this->errors, 'stylesheet' => $this->stylesheet, 'template' => $this->template, ) ); } return; } // Set the parent. Pass the current instance so we can do the crazy checks above and assess errors. $this->parent = new WP_Theme( $this->template, isset( $theme_root_template ) ? $theme_root_template : $this->theme_root, $this ); } if ( wp_paused_themes()->get( $this->stylesheet ) && ( ! is_wp_error( $this->errors ) || ! isset( $this->errors->errors['theme_paused'] ) ) ) { $this->errors = new WP_Error( 'theme_paused', __( 'This theme failed to load properly and was paused within the admin backend.' ) ); } // We're good. If we didn't retrieve from cache, set it. if ( ! is_array( $cache ) ) { $cache = array( 'block_theme' => $this->is_block_theme(), 'headers' => $this->headers, 'errors' => $this->errors, 'stylesheet' => $this->stylesheet, 'template' => $this->template, ); // If the parent theme is in another root, we'll want to cache this. Avoids an entire branch of filesystem calls above. if ( isset( $theme_root_template ) ) { $cache['theme_root_template'] = $theme_root_template; } $this->cache_add( 'theme', $cache ); } } /** * When converting the object to a string, the theme name is returned. * * @since 3.4.0 * * @return string Theme name, ready for display (translated) */ public function __toString() { return (string) $this->display( 'Name' ); } /** * __isset() magic method for properties formerly returned by current_theme_info() * * @since 3.4.0 * * @param string $offset Property to check if set. * @return bool Whether the given property is set. */ public function __isset( $offset ) { static $properties = array( 'name', 'title', 'version', 'parent_theme', 'template_dir', 'stylesheet_dir', 'template', 'stylesheet', 'screenshot', 'description', 'author', 'tags', 'theme_root', 'theme_root_uri', ); return in_array( $offset, $properties, true ); } /** * __get() magic method for properties formerly returned by current_theme_info() * * @since 3.4.0 * * @param string $offset Property to get. * @return mixed Property value. */ public function __get( $offset ) { switch ( $offset ) { case 'name': case 'title': return $this->get( 'Name' ); case 'version': return $this->get( 'Version' ); case 'parent_theme': return $this->parent() ? $this->parent()->get( 'Name' ) : ''; case 'template_dir': return $this->get_template_directory(); case 'stylesheet_dir': return $this->get_stylesheet_directory(); case 'template': return $this->get_template(); case 'stylesheet': return $this->get_stylesheet(); case 'screenshot': return $this->get_screenshot( 'relative' ); // 'author' and 'description' did not previously return translated data. case 'description': return $this->display( 'Description' ); case 'author': return $this->display( 'Author' ); case 'tags': return $this->get( 'Tags' ); case 'theme_root': return $this->get_theme_root(); case 'theme_root_uri': return $this->get_theme_root_uri(); // For cases where the array was converted to an object. default: return $this->offsetGet( $offset ); } } /** * Method to implement ArrayAccess for keys formerly returned by get_themes() * * @since 3.4.0 * * @param mixed $offset * @param mixed $value */ #[ReturnTypeWillChange] public function offsetSet( $offset, $value ) {} /** * Method to implement ArrayAccess for keys formerly returned by get_themes() * * @since 3.4.0 * * @param mixed $offset */ #[ReturnTypeWillChange] public function offsetUnset( $offset ) {} /** * Method to implement ArrayAccess for keys formerly returned by get_themes() * * @since 3.4.0 * * @param mixed $offset * @return bool */ #[ReturnTypeWillChange] public function offsetExists( $offset ) { static $keys = array( 'Name', 'Version', 'Status', 'Title', 'Author', 'Author Name', 'Author URI', 'Description', 'Template', 'Stylesheet', 'Template Files', 'Stylesheet Files', 'Template Dir', 'Stylesheet Dir', 'Screenshot', 'Tags', 'Theme Root', 'Theme Root URI', 'Parent Theme', ); return in_array( $offset, $keys, true ); } /** * Method to implement ArrayAccess for keys formerly returned by get_themes(). * * Author, Author Name, Author URI, and Description did not previously return * translated data. We are doing so now as it is safe to do. However, as * Name and Title could have been used as the key for get_themes(), both remain * untranslated for back compatibility. This means that ['Name'] is not ideal, * and care should be taken to use `$theme::display( 'Name' )` to get a properly * translated header. * * @since 3.4.0 * * @param mixed $offset * @return mixed */ #[ReturnTypeWillChange] public function offsetGet( $offset ) { switch ( $offset ) { case 'Name': case 'Title': /* * See note above about using translated data. get() is not ideal. * It is only for backward compatibility. Use display(). */ return $this->get( 'Name' ); case 'Author': return $this->display( 'Author' ); case 'Author Name': return $this->display( 'Author', false ); case 'Author URI': return $this->display( 'AuthorURI' ); case 'Description': return $this->display( 'Description' ); case 'Version': case 'Status': return $this->get( $offset ); case 'Template': return $this->get_template(); case 'Stylesheet': return $this->get_stylesheet(); case 'Template Files': return $this->get_files( 'php', 1, true ); case 'Stylesheet Files': return $this->get_files( 'css', 0, false ); case 'Template Dir': return $this->get_template_directory(); case 'Stylesheet Dir': return $this->get_stylesheet_directory(); case 'Screenshot': return $this->get_screenshot( 'relative' ); case 'Tags': return $this->get( 'Tags' ); case 'Theme Root': return $this->get_theme_root(); case 'Theme Root URI': return $this->get_theme_root_uri(); case 'Parent Theme': return $this->parent() ? $this->parent()->get( 'Name' ) : ''; default: return null; } } /** * Returns errors property. * * @since 3.4.0 * * @return WP_Error|false WP_Error if there are errors, or false. */ public function errors() { return is_wp_error( $this->errors ) ? $this->errors : false; } /** * Determines whether the theme exists. * * A theme with errors exists. A theme with the error of 'theme_not_found', * meaning that the theme's directory was not found, does not exist. * * @since 3.4.0 * * @return bool Whether the theme exists. */ public function exists() { return ! ( $this->errors() && in_array( 'theme_not_found', $this->errors()->get_error_codes(), true ) ); } /** * Returns reference to the parent theme. * * @since 3.4.0 * * @return WP_Theme|false Parent theme, or false if the active theme is not a child theme. */ public function parent() { return isset( $this->parent ) ? $this->parent : false; } /** * Adds theme data to cache. * * Cache entries keyed by the theme and the type of data. * * @since 3.4.0 * * @param string $key Type of data to store (theme, screenshot, headers, post_templates) * @param array|string $data Data to store * @return bool Return value from wp_cache_add() */ private function cache_add( $key, $data ) { return wp_cache_add( $key . '-' . $this->cache_hash, $data, 'themes', self::$cache_expiration ); } /** * Gets theme data from cache. * * Cache entries are keyed by the theme and the type of data. * * @since 3.4.0 * * @param string $key Type of data to retrieve (theme, screenshot, headers, post_templates) * @return mixed Retrieved data */ private function cache_get( $key ) { return wp_cache_get( $key . '-' . $this->cache_hash, 'themes' ); } /** * Clears the cache for the theme. * * @since 3.4.0 */ public function cache_delete() { foreach ( array( 'theme', 'screenshot', 'headers', 'post_templates' ) as $key ) { wp_cache_delete( $key . '-' . $this->cache_hash, 'themes' ); } $this->template = null; $this->textdomain_loaded = null; $this->theme_root_uri = null; $this->parent = null; $this->errors = null; $this->headers_sanitized = null; $this->name_translated = null; $this->block_theme = null; $this->headers = array(); $this->__construct( $this->stylesheet, $this->theme_root ); } /** * Gets a raw, unformatted theme header. * * The header is sanitized, but is not translated, and is not marked up for display. * To get a theme header for display, use the display() method. * * Use the get_template() method, not the 'Template' header, for finding the template. * The 'Template' header is only good for what was written in the style.css, while * get_template() takes into account where WordPress actually located the theme and * whether it is actually valid. * * @since 3.4.0 * * @param string $header Theme header. Name, Description, Author, Version, ThemeURI, AuthorURI, Status, Tags. * @return string|array|false String or array (for Tags header) on success, false on failure. */ public function get( $header ) { if ( ! isset( $this->headers[ $header ] ) ) { return false; } if ( ! isset( $this->headers_sanitized ) ) { $this->headers_sanitized = $this->cache_get( 'headers' ); if ( ! is_array( $this->headers_sanitized ) ) { $this->headers_sanitized = array(); } } if ( isset( $this->headers_sanitized[ $header ] ) ) { return $this->headers_sanitized[ $header ]; } // If themes are a persistent group, sanitize everything and cache it. One cache add is better than many cache sets. if ( self::$persistently_cache ) { foreach ( array_keys( $this->headers ) as $_header ) { $this->headers_sanitized[ $_header ] = $this->sanitize_header( $_header, $this->headers[ $_header ] ); } $this->cache_add( 'headers', $this->headers_sanitized ); } else { $this->headers_sanitized[ $header ] = $this->sanitize_header( $header, $this->headers[ $header ] ); } return $this->headers_sanitized[ $header ]; } /** * Gets a theme header, formatted and translated for display. * * @since 3.4.0 * * @param string $header Theme header. Name, Description, Author, Version, ThemeURI, AuthorURI, Status, Tags. * @param bool $markup Optional. Whether to mark up the header. Defaults to true. * @param bool $translate Optional. Whether to translate the header. Defaults to true. * @return string|array|false Processed header. An array for Tags if `$markup` is false, string otherwise. * False on failure. */ public function display( $header, $markup = true, $translate = true ) { $value = $this->get( $header ); if ( false === $value ) { return false; } if ( $translate && ( empty( $value ) || ! $this->load_textdomain() ) ) { $translate = false; } if ( $translate ) { $value = $this->translate_header( $header, $value ); } if ( $markup ) { $value = $this->markup_header( $header, $value, $translate ); } return $value; } /** * Sanitizes a theme header. * * @since 3.4.0 * @since 5.4.0 Added support for `Requires at least` and `Requires PHP` headers. * @since 6.1.0 Added support for `Update URI` header. * * @param string $header Theme header. Accepts 'Name', 'Description', 'Author', 'Version', * 'ThemeURI', 'AuthorURI', 'Status', 'Tags', 'RequiresWP', 'RequiresPHP', * 'UpdateURI'. * @param string $value Value to sanitize. * @return string|array An array for Tags header, string otherwise. */ private function sanitize_header( $header, $value ) { switch ( $header ) { case 'Status': if ( ! $value ) { $value = 'publish'; break; } // Fall through otherwise. case 'Name': static $header_tags = array( 'abbr' => array( 'title' => true ), 'acronym' => array( 'title' => true ), 'code' => true, 'em' => true, 'strong' => true, ); $value = wp_kses( $value, $header_tags ); break; case 'Author': // There shouldn't be anchor tags in Author, but some themes like to be challenging. case 'Description': static $header_tags_with_a = array( 'a' => array( 'href' => true, 'title' => true, ), 'abbr' => array( 'title' => true ), 'acronym' => array( 'title' => true ), 'code' => true, 'em' => true, 'strong' => true, ); $value = wp_kses( $value, $header_tags_with_a ); break; case 'ThemeURI': case 'AuthorURI': $value = sanitize_url( $value ); break; case 'Tags': $value = array_filter( array_map( 'trim', explode( ',', strip_tags( $value ) ) ) ); break; case 'Version': case 'RequiresWP': case 'RequiresPHP': case 'UpdateURI': $value = strip_tags( $value ); break; } return $value; } /** * Marks up a theme header. * * @since 3.4.0 * * @param string $header Theme header. Name, Description, Author, Version, ThemeURI, AuthorURI, Status, Tags. * @param string|array $value Value to mark up. An array for Tags header, string otherwise. * @param string $translate Whether the header has been translated. * @return string Value, marked up. */ private function markup_header( $header, $value, $translate ) { switch ( $header ) { case 'Name': if ( empty( $value ) ) { $value = esc_html( $this->get_stylesheet() ); } break; case 'Description': $value = wptexturize( $value ); break; case 'Author': if ( $this->get( 'AuthorURI' ) ) { $value = sprintf( '%2$s', $this->display( 'AuthorURI', true, $translate ), $value ); } elseif ( ! $value ) { $value = __( 'Anonymous' ); } break; case 'Tags': static $comma = null; if ( ! isset( $comma ) ) { $comma = wp_get_list_item_separator(); } $value = implode( $comma, $value ); break; case 'ThemeURI': case 'AuthorURI': $value = esc_url( $value ); break; } return $value; } /** * Translates a theme header. * * @since 3.4.0 * * @param string $header Theme header. Name, Description, Author, Version, ThemeURI, AuthorURI, Status, Tags. * @param string|array $value Value to translate. An array for Tags header, string otherwise. * @return string|array Translated value. An array for Tags header, string otherwise. */ private function translate_header( $header, $value ) { switch ( $header ) { case 'Name': // Cached for sorting reasons. if ( isset( $this->name_translated ) ) { return $this->name_translated; } // phpcs:ignore WordPress.WP.I18n.LowLevelTranslationFunction,WordPress.WP.I18n.NonSingularStringLiteralText,WordPress.WP.I18n.NonSingularStringLiteralDomain $this->name_translated = translate( $value, $this->get( 'TextDomain' ) ); return $this->name_translated; case 'Tags': if ( empty( $value ) || ! function_exists( 'get_theme_feature_list' ) ) { return $value; } static $tags_list; if ( ! isset( $tags_list ) ) { $tags_list = array( // As of 4.6, deprecated tags which are only used to provide translation for older themes. 'black' => __( 'Black' ), 'blue' => __( 'Blue' ), 'brown' => __( 'Brown' ), 'gray' => __( 'Gray' ), 'green' => __( 'Green' ), 'orange' => __( 'Orange' ), 'pink' => __( 'Pink' ), 'purple' => __( 'Purple' ), 'red' => __( 'Red' ), 'silver' => __( 'Silver' ), 'tan' => __( 'Tan' ), 'white' => __( 'White' ), 'yellow' => __( 'Yellow' ), 'dark' => _x( 'Dark', 'color scheme' ), 'light' => _x( 'Light', 'color scheme' ), 'fixed-layout' => __( 'Fixed Layout' ), 'fluid-layout' => __( 'Fluid Layout' ), 'responsive-layout' => __( 'Responsive Layout' ), 'blavatar' => __( 'Blavatar' ), 'photoblogging' => __( 'Photoblogging' ), 'seasonal' => __( 'Seasonal' ), ); $feature_list = get_theme_feature_list( false ); // No API. foreach ( $feature_list as $tags ) { $tags_list += $tags; } } foreach ( $value as &$tag ) { if ( isset( $tags_list[ $tag ] ) ) { $tag = $tags_list[ $tag ]; } elseif ( isset( self::$tag_map[ $tag ] ) ) { $tag = $tags_list[ self::$tag_map[ $tag ] ]; } } return $value; default: // phpcs:ignore WordPress.WP.I18n.LowLevelTranslationFunction,WordPress.WP.I18n.NonSingularStringLiteralText,WordPress.WP.I18n.NonSingularStringLiteralDomain $value = translate( $value, $this->get( 'TextDomain' ) ); } return $value; } /** * Returns the directory name of the theme's "stylesheet" files, inside the theme root. * * In the case of a child theme, this is directory name of the child theme. * Otherwise, get_stylesheet() is the same as get_template(). * * @since 3.4.0 * * @return string Stylesheet */ public function get_stylesheet() { return $this->stylesheet; } /** * Returns the directory name of the theme's "template" files, inside the theme root. * * In the case of a child theme, this is the directory name of the parent theme. * Otherwise, the get_template() is the same as get_stylesheet(). * * @since 3.4.0 * * @return string Template */ public function get_template() { return $this->template; } /** * Returns the absolute path to the directory of a theme's "stylesheet" files. * * In the case of a child theme, this is the absolute path to the directory * of the child theme's files. * * @since 3.4.0 * * @return string Absolute path of the stylesheet directory. */ public function get_stylesheet_directory() { if ( $this->errors() && in_array( 'theme_root_missing', $this->errors()->get_error_codes(), true ) ) { return ''; } return $this->theme_root . '/' . $this->stylesheet; } /** * Returns the absolute path to the directory of a theme's "template" files. * * In the case of a child theme, this is the absolute path to the directory * of the parent theme's files. * * @since 3.4.0 * * @return string Absolute path of the template directory. */ public function get_template_directory() { if ( $this->parent() ) { $theme_root = $this->parent()->theme_root; } else { $theme_root = $this->theme_root; } return $theme_root . '/' . $this->template; } /** * Returns the URL to the directory of a theme's "stylesheet" files. * * In the case of a child theme, this is the URL to the directory of the * child theme's files. * * @since 3.4.0 * * @return string URL to the stylesheet directory. */ public function get_stylesheet_directory_uri() { return $this->get_theme_root_uri() . '/' . str_replace( '%2F', '/', rawurlencode( $this->stylesheet ) ); } /** * Returns the URL to the directory of a theme's "template" files. * * In the case of a child theme, this is the URL to the directory of the * parent theme's files. * * @since 3.4.0 * * @return string URL to the template directory. */ public function get_template_directory_uri() { if ( $this->parent() ) { $theme_root_uri = $this->parent()->get_theme_root_uri(); } else { $theme_root_uri = $this->get_theme_root_uri(); } return $theme_root_uri . '/' . str_replace( '%2F', '/', rawurlencode( $this->template ) ); } /** * Returns the absolute path to the directory of the theme root. * * This is typically the absolute path to wp-content/themes. * * @since 3.4.0 * * @return string Theme root. */ public function get_theme_root() { return $this->theme_root; } /** * Returns the URL to the directory of the theme root. * * This is typically the absolute URL to wp-content/themes. This forms the basis * for all other URLs returned by WP_Theme, so we pass it to the public function * get_theme_root_uri() and allow it to run the {@see 'theme_root_uri'} filter. * * @since 3.4.0 * * @return string Theme root URI. */ public function get_theme_root_uri() { if ( ! isset( $this->theme_root_uri ) ) { $this->theme_root_uri = get_theme_root_uri( $this->stylesheet, $this->theme_root ); } return $this->theme_root_uri; } /** * Returns the main screenshot file for the theme. * * The main screenshot is called screenshot.png. gif and jpg extensions are also allowed. * * Screenshots for a theme must be in the stylesheet directory. (In the case of child * themes, parent theme screenshots are not inherited.) * * @since 3.4.0 * * @param string $uri Type of URL to return, either 'relative' or an absolute URI. Defaults to absolute URI. * @return string|false Screenshot file. False if the theme does not have a screenshot. */ public function get_screenshot( $uri = 'uri' ) { $screenshot = $this->cache_get( 'screenshot' ); if ( $screenshot ) { if ( 'relative' === $uri ) { return $screenshot; } return $this->get_stylesheet_directory_uri() . '/' . $screenshot; } elseif ( 0 === $screenshot ) { return false; } foreach ( array( 'png', 'gif', 'jpg', 'jpeg', 'webp' ) as $ext ) { if ( file_exists( $this->get_stylesheet_directory() . "/screenshot.$ext" ) ) { $this->cache_add( 'screenshot', 'screenshot.' . $ext ); if ( 'relative' === $uri ) { return 'screenshot.' . $ext; } return $this->get_stylesheet_directory_uri() . '/' . 'screenshot.' . $ext; } } $this->cache_add( 'screenshot', 0 ); return false; } /** * Returns files in the theme's directory. * * @since 3.4.0 * * @param string[]|string $type Optional. Array of extensions to find, string of a single extension, * or null for all extensions. Default null. * @param int $depth Optional. How deep to search for files. Defaults to a flat scan (0 depth). * -1 depth is infinite. * @param bool $search_parent Optional. Whether to return parent files. Default false. * @return string[] Array of files, keyed by the path to the file relative to the theme's directory, with the values * being absolute paths. */ public function get_files( $type = null, $depth = 0, $search_parent = false ) { $files = (array) self::scandir( $this->get_stylesheet_directory(), $type, $depth ); if ( $search_parent && $this->parent() ) { $files += (array) self::scandir( $this->get_template_directory(), $type, $depth ); } return array_filter( $files ); } /** * Returns the theme's post templates. * * @since 4.7.0 * @since 5.8.0 Include block templates. * * @return array[] Array of page template arrays, keyed by post type and filename, * with the value of the translated header name. */ public function get_post_templates() { // If you screw up your active theme and we invalidate your parent, most things still work. Let it slide. if ( $this->errors() && $this->errors()->get_error_codes() !== array( 'theme_parent_invalid' ) ) { return array(); } $post_templates = $this->cache_get( 'post_templates' ); if ( ! is_array( $post_templates ) ) { $post_templates = array(); $files = (array) $this->get_files( 'php', 1, true ); foreach ( $files as $file => $full_path ) { if ( ! preg_match( '|Template Name:(.*)$|mi', file_get_contents( $full_path ), $header ) ) { continue; } $types = array( 'page' ); if ( preg_match( '|Template Post Type:(.*)$|mi', file_get_contents( $full_path ), $type ) ) { $types = explode( ',', _cleanup_header_comment( $type[1] ) ); } foreach ( $types as $type ) { $type = sanitize_key( $type ); if ( ! isset( $post_templates[ $type ] ) ) { $post_templates[ $type ] = array(); } $post_templates[ $type ][ $file ] = _cleanup_header_comment( $header[1] ); } } if ( current_theme_supports( 'block-templates' ) ) { $block_templates = get_block_templates( array(), 'wp_template' ); foreach ( get_post_types( array( 'public' => true ) ) as $type ) { foreach ( $block_templates as $block_template ) { if ( ! $block_template->is_custom ) { continue; } if ( isset( $block_template->post_types ) && ! in_array( $type, $block_template->post_types, true ) ) { continue; } $post_templates[ $type ][ $block_template->slug ] = $block_template->title; } } } $this->cache_add( 'post_templates', $post_templates ); } if ( $this->load_textdomain() ) { foreach ( $post_templates as &$post_type ) { foreach ( $post_type as &$post_template ) { $post_template = $this->translate_header( 'Template Name', $post_template ); } } } return $post_templates; } /** * Returns the theme's post templates for a given post type. * * @since 3.4.0 * @since 4.7.0 Added the `$post_type` parameter. * * @param WP_Post|null $post Optional. The post being edited, provided for context. * @param string $post_type Optional. Post type to get the templates for. Default 'page'. * If a post is provided, its post type is used. * @return string[] Array of template header names keyed by the template file name. */ public function get_page_templates( $post = null, $post_type = 'page' ) { if ( $post ) { $post_type = get_post_type( $post ); } $post_templates = $this->get_post_templates(); $post_templates = isset( $post_templates[ $post_type ] ) ? $post_templates[ $post_type ] : array(); /** * Filters list of page templates for a theme. * * @since 4.9.6 * * @param string[] $post_templates Array of template header names keyed by the template file name. * @param WP_Theme $theme The theme object. * @param WP_Post|null $post The post being edited, provided for context, or null. * @param string $post_type Post type to get the templates for. */ $post_templates = (array) apply_filters( 'theme_templates', $post_templates, $this, $post, $post_type ); /** * Filters list of page templates for a theme. * * The dynamic portion of the hook name, `$post_type`, refers to the post type. * * Possible hook names include: * * - `theme_post_templates` * - `theme_page_templates` * - `theme_attachment_templates` * * @since 3.9.0 * @since 4.4.0 Converted to allow complete control over the `$page_templates` array. * @since 4.7.0 Added the `$post_type` parameter. * * @param string[] $post_templates Array of template header names keyed by the template file name. * @param WP_Theme $theme The theme object. * @param WP_Post|null $post The post being edited, provided for context, or null. * @param string $post_type Post type to get the templates for. */ $post_templates = (array) apply_filters( "theme_{$post_type}_templates", $post_templates, $this, $post, $post_type ); return $post_templates; } /** * Scans a directory for files of a certain extension. * * @since 3.4.0 * * @param string $path Absolute path to search. * @param array|string|null $extensions Optional. Array of extensions to find, string of a single extension, * or null for all extensions. Default null. * @param int $depth Optional. How many levels deep to search for files. Accepts 0, 1+, or * -1 (infinite depth). Default 0. * @param string $relative_path Optional. The basename of the absolute path. Used to control the * returned path for the found files, particularly when this function * recurses to lower depths. Default empty. * @return string[]|false Array of files, keyed by the path to the file relative to the `$path` directory prepended * with `$relative_path`, with the values being absolute paths. False otherwise. */ private static function scandir( $path, $extensions = null, $depth = 0, $relative_path = '' ) { if ( ! is_dir( $path ) ) { return false; } if ( $extensions ) { $extensions = (array) $extensions; $_extensions = implode( '|', $extensions ); } $relative_path = trailingslashit( $relative_path ); if ( '/' === $relative_path ) { $relative_path = ''; } $results = scandir( $path ); $files = array(); /** * Filters the array of excluded directories and files while scanning theme folder. * * @since 4.7.4 * * @param string[] $exclusions Array of excluded directories and files. */ $exclusions = (array) apply_filters( 'theme_scandir_exclusions', array( 'CVS', 'node_modules', 'vendor', 'bower_components' ) ); foreach ( $results as $result ) { if ( '.' === $result[0] || in_array( $result, $exclusions, true ) ) { continue; } if ( is_dir( $path . '/' . $result ) ) { if ( ! $depth ) { continue; } $found = self::scandir( $path . '/' . $result, $extensions, $depth - 1, $relative_path . $result ); $files = array_merge_recursive( $files, $found ); } elseif ( ! $extensions || preg_match( '~\.(' . $_extensions . ')$~', $result ) ) { $files[ $relative_path . $result ] = $path . '/' . $result; } } return $files; } /** * Loads the theme's textdomain. * * Translation files are not inherited from the parent theme. TODO: If this fails for the * child theme, it should probably try to load the parent theme's translations. * * @since 3.4.0 * * @return bool True if the textdomain was successfully loaded or has already been loaded. * False if no textdomain was specified in the file headers, or if the domain could not be loaded. */ public function load_textdomain() { if ( isset( $this->textdomain_loaded ) ) { return $this->textdomain_loaded; } $textdomain = $this->get( 'TextDomain' ); if ( ! $textdomain ) { $this->textdomain_loaded = false; return false; } if ( is_textdomain_loaded( $textdomain ) ) { $this->textdomain_loaded = true; return true; } $path = $this->get_stylesheet_directory(); $domainpath = $this->get( 'DomainPath' ); if ( $domainpath ) { $path .= $domainpath; } else { $path .= '/languages'; } $this->textdomain_loaded = load_theme_textdomain( $textdomain, $path ); return $this->textdomain_loaded; } /** * Determines whether the theme is allowed (multisite only). * * @since 3.4.0 * * @param string $check Optional. Whether to check only the 'network'-wide settings, the 'site' * settings, or 'both'. Defaults to 'both'. * @param int $blog_id Optional. Ignored if only network-wide settings are checked. Defaults to current site. * @return bool Whether the theme is allowed for the network. Returns true in single-site. */ public function is_allowed( $check = 'both', $blog_id = null ) { if ( ! is_multisite() ) { return true; } if ( 'both' === $check || 'network' === $check ) { $allowed = self::get_allowed_on_network(); if ( ! empty( $allowed[ $this->get_stylesheet() ] ) ) { return true; } } if ( 'both' === $check || 'site' === $check ) { $allowed = self::get_allowed_on_site( $blog_id ); if ( ! empty( $allowed[ $this->get_stylesheet() ] ) ) { return true; } } return false; } /** * Returns whether this theme is a block-based theme or not. * * @since 5.9.0 * * @return bool */ public function is_block_theme() { if ( isset( $this->block_theme ) ) { return $this->block_theme; } $paths_to_index_block_template = array( $this->get_file_path( '/block-templates/index.html' ), $this->get_file_path( '/templates/index.html' ), ); $this->block_theme = false; foreach ( $paths_to_index_block_template as $path_to_index_block_template ) { if ( is_file( $path_to_index_block_template ) && is_readable( $path_to_index_block_template ) ) { $this->block_theme = true; break; } } return $this->block_theme; } /** * Retrieves the path of a file in the theme. * * Searches in the stylesheet directory before the template directory so themes * which inherit from a parent theme can just override one file. * * @since 5.9.0 * * @param string $file Optional. File to search for in the stylesheet directory. * @return string The path of the file. */ public function get_file_path( $file = '' ) { $file = ltrim( $file, '/' ); $stylesheet_directory = $this->get_stylesheet_directory(); $template_directory = $this->get_template_directory(); if ( empty( $file ) ) { $path = $stylesheet_directory; } elseif ( file_exists( $stylesheet_directory . '/' . $file ) ) { $path = $stylesheet_directory . '/' . $file; } else { $path = $template_directory . '/' . $file; } /** This filter is documented in wp-includes/link-template.php */ return apply_filters( 'theme_file_path', $path, $file ); } /** * Determines the latest WordPress default theme that is installed. * * This hits the filesystem. * * @since 4.4.0 * * @return WP_Theme|false Object, or false if no theme is installed, which would be bad. */ public static function get_core_default_theme() { foreach ( array_reverse( self::$default_themes ) as $slug => $name ) { $theme = wp_get_theme( $slug ); if ( $theme->exists() ) { return $theme; } } return false; } /** * Returns array of stylesheet names of themes allowed on the site or network. * * @since 3.4.0 * * @param int $blog_id Optional. ID of the site. Defaults to the current site. * @return string[] Array of stylesheet names. */ public static function get_allowed( $blog_id = null ) { /** * Filters the array of themes allowed on the network. * * Site is provided as context so that a list of network allowed themes can * be filtered further. * * @since 4.5.0 * * @param string[] $allowed_themes An array of theme stylesheet names. * @param int $blog_id ID of the site. */ $network = (array) apply_filters( 'network_allowed_themes', self::get_allowed_on_network(), $blog_id ); return $network + self::get_allowed_on_site( $blog_id ); } /** * Returns array of stylesheet names of themes allowed on the network. * * @since 3.4.0 * * @return string[] Array of stylesheet names. */ public static function get_allowed_on_network() { static $allowed_themes; if ( ! isset( $allowed_themes ) ) { $allowed_themes = (array) get_site_option( 'allowedthemes' ); } /** * Filters the array of themes allowed on the network. * * @since MU (3.0.0) * * @param string[] $allowed_themes An array of theme stylesheet names. */ $allowed_themes = apply_filters( 'allowed_themes', $allowed_themes ); return $allowed_themes; } /** * Returns array of stylesheet names of themes allowed on the site. * * @since 3.4.0 * * @param int $blog_id Optional. ID of the site. Defaults to the current site. * @return string[] Array of stylesheet names. */ public static function get_allowed_on_site( $blog_id = null ) { static $allowed_themes = array(); if ( ! $blog_id || ! is_multisite() ) { $blog_id = get_current_blog_id(); } if ( isset( $allowed_themes[ $blog_id ] ) ) { /** * Filters the array of themes allowed on the site. * * @since 4.5.0 * * @param string[] $allowed_themes An array of theme stylesheet names. * @param int $blog_id ID of the site. Defaults to current site. */ return (array) apply_filters( 'site_allowed_themes', $allowed_themes[ $blog_id ], $blog_id ); } $current = get_current_blog_id() == $blog_id; if ( $current ) { $allowed_themes[ $blog_id ] = get_option( 'allowedthemes' ); } else { switch_to_blog( $blog_id ); $allowed_themes[ $blog_id ] = get_option( 'allowedthemes' ); restore_current_blog(); } // This is all super old MU back compat joy. // 'allowedthemes' keys things by stylesheet. 'allowed_themes' keyed things by name. if ( false === $allowed_themes[ $blog_id ] ) { if ( $current ) { $allowed_themes[ $blog_id ] = get_option( 'allowed_themes' ); } else { switch_to_blog( $blog_id ); $allowed_themes[ $blog_id ] = get_option( 'allowed_themes' ); restore_current_blog(); } if ( ! is_array( $allowed_themes[ $blog_id ] ) || empty( $allowed_themes[ $blog_id ] ) ) { $allowed_themes[ $blog_id ] = array(); } else { $converted = array(); $themes = wp_get_themes(); foreach ( $themes as $stylesheet => $theme_data ) { if ( isset( $allowed_themes[ $blog_id ][ $theme_data->get( 'Name' ) ] ) ) { $converted[ $stylesheet ] = true; } } $allowed_themes[ $blog_id ] = $converted; } // Set the option so we never have to go through this pain again. if ( is_admin() && $allowed_themes[ $blog_id ] ) { if ( $current ) { update_option( 'allowedthemes', $allowed_themes[ $blog_id ] ); delete_option( 'allowed_themes' ); } else { switch_to_blog( $blog_id ); update_option( 'allowedthemes', $allowed_themes[ $blog_id ] ); delete_option( 'allowed_themes' ); restore_current_blog(); } } } /** This filter is documented in wp-includes/class-wp-theme.php */ return (array) apply_filters( 'site_allowed_themes', $allowed_themes[ $blog_id ], $blog_id ); } /** * Enables a theme for all sites on the current network. * * @since 4.6.0 * * @param string|string[] $stylesheets Stylesheet name or array of stylesheet names. */ public static function network_enable_theme( $stylesheets ) { if ( ! is_multisite() ) { return; } if ( ! is_array( $stylesheets ) ) { $stylesheets = array( $stylesheets ); } $allowed_themes = get_site_option( 'allowedthemes' ); foreach ( $stylesheets as $stylesheet ) { $allowed_themes[ $stylesheet ] = true; } update_site_option( 'allowedthemes', $allowed_themes ); } /** * Disables a theme for all sites on the current network. * * @since 4.6.0 * * @param string|string[] $stylesheets Stylesheet name or array of stylesheet names. */ public static function network_disable_theme( $stylesheets ) { if ( ! is_multisite() ) { return; } if ( ! is_array( $stylesheets ) ) { $stylesheets = array( $stylesheets ); } $allowed_themes = get_site_option( 'allowedthemes' ); foreach ( $stylesheets as $stylesheet ) { if ( isset( $allowed_themes[ $stylesheet ] ) ) { unset( $allowed_themes[ $stylesheet ] ); } } update_site_option( 'allowedthemes', $allowed_themes ); } /** * Sorts themes by name. * * @since 3.4.0 * * @param WP_Theme[] $themes Array of theme objects to sort (passed by reference). */ public static function sort_by_name( &$themes ) { if ( 0 === strpos( get_user_locale(), 'en_' ) ) { uasort( $themes, array( 'WP_Theme', '_name_sort' ) ); } else { foreach ( $themes as $key => $theme ) { $theme->translate_header( 'Name', $theme->headers['Name'] ); } uasort( $themes, array( 'WP_Theme', '_name_sort_i18n' ) ); } } /** * Callback function for usort() to naturally sort themes by name. * * Accesses the Name header directly from the class for maximum speed. * Would choke on HTML but we don't care enough to slow it down with strip_tags(). * * @since 3.4.0 * * @param WP_Theme $a First theme. * @param WP_Theme $b Second theme. * @return int Negative if `$a` falls lower in the natural order than `$b`. Zero if they fall equally. * Greater than 0 if `$a` falls higher in the natural order than `$b`. Used with usort(). */ private static function _name_sort( $a, $b ) { return strnatcasecmp( $a->headers['Name'], $b->headers['Name'] ); } /** * Callback function for usort() to naturally sort themes by translated name. * * @since 3.4.0 * * @param WP_Theme $a First theme. * @param WP_Theme $b Second theme. * @return int Negative if `$a` falls lower in the natural order than `$b`. Zero if they fall equally. * Greater than 0 if `$a` falls higher in the natural order than `$b`. Used with usort(). */ private static function _name_sort_i18n( $a, $b ) { return strnatcasecmp( $a->name_translated, $b->name_translated ); } } /** * Dependencies API: _WP_Dependency class * * @since 4.7.0 * * @package WordPress * @subpackage Dependencies */ /** * Class _WP_Dependency * * Helper class to register a handle and associated data. * * @access private * @since 2.6.0 */ #[AllowDynamicProperties] class _WP_Dependency { /** * The handle name. * * @since 2.6.0 * @var string */ public $handle; /** * The handle source. * * If source is set to false, the item is an alias of other items it depends on. * * @since 2.6.0 * @var string|false */ public $src; /** * An array of handle dependencies. * * @since 2.6.0 * @var string[] */ public $deps = array(); /** * The handle version. * * Used for cache-busting. * * @since 2.6.0 * @var bool|string */ public $ver = false; /** * Additional arguments for the handle. * * @since 2.6.0 * @var array */ public $args = null; // Custom property, such as $in_footer or $media. /** * Extra data to supply to the handle. * * @since 2.6.0 * @var array */ public $extra = array(); /** * Translation textdomain set for this dependency. * * @since 5.0.0 * @var string */ public $textdomain; /** * Translation path set for this dependency. * * @since 5.0.0 * @var string */ public $translations_path; /** * Setup dependencies. * * @since 2.6.0 * @since 5.3.0 Formalized the existing `...$args` parameter by adding it * to the function signature. * * @param mixed ...$args Dependency information. */ public function __construct( ...$args ) { list( $this->handle, $this->src, $this->deps, $this->ver, $this->args ) = $args; if ( ! is_array( $this->deps ) ) { $this->deps = array(); } } /** * Add handle data. * * @since 2.6.0 * * @param string $name The data key to add. * @param mixed $data The data value to add. * @return bool False if not scalar, true otherwise. */ public function add_data( $name, $data ) { if ( ! is_scalar( $name ) ) { return false; } $this->extra[ $name ] = $data; return true; } /** * Sets the translation domain for this dependency. * * @since 5.0.0 * * @param string $domain The translation textdomain. * @param string $path Optional. The full file path to the directory containing translation files. * @return bool False if $domain is not a string, true otherwise. */ public function set_translations( $domain, $path = '' ) { if ( ! is_string( $domain ) ) { return false; } $this->textdomain = $domain; $this->translations_path = $path; return true; } } /** * Dependencies API: Scripts functions * * @since 2.6.0 * * @package WordPress * @subpackage Dependencies */ /** * Initializes $wp_scripts if it has not been set. * * @global WP_Scripts $wp_scripts * * @since 4.2.0 * * @return WP_Scripts WP_Scripts instance. */ function wp_scripts() { global $wp_scripts; if ( ! ( $wp_scripts instanceof WP_Scripts ) ) { $wp_scripts = new WP_Scripts(); } return $wp_scripts; } /** * Helper function to output a _doing_it_wrong message when applicable. * * @ignore * @since 4.2.0 * @since 5.5.0 Added the `$handle` parameter. * * @param string $function_name Function name. * @param string $handle Optional. Name of the script or stylesheet that was * registered or enqueued too early. Default empty. */ function _wp_scripts_maybe_doing_it_wrong( $function_name, $handle = '' ) { if ( did_action( 'init' ) || did_action( 'wp_enqueue_scripts' ) || did_action( 'admin_enqueue_scripts' ) || did_action( 'login_enqueue_scripts' ) ) { return; } $message = sprintf( /* translators: 1: wp_enqueue_scripts, 2: admin_enqueue_scripts, 3: login_enqueue_scripts */ __( 'Scripts and styles should not be registered or enqueued until the %1$s, %2$s, or %3$s hooks.' ), 'wp_enqueue_scripts', 'admin_enqueue_scripts', 'login_enqueue_scripts' ); if ( $handle ) { $message .= ' ' . sprintf( /* translators: %s: Name of the script or stylesheet. */ __( 'This notice was triggered by the %s handle.' ), '' . $handle . '' ); } _doing_it_wrong( $function_name, $message, '3.3.0' ); } /** * Prints scripts in document head that are in the $handles queue. * * Called by admin-header.php and {@see 'wp_head'} hook. Since it is called by wp_head on every page load, * the function does not instantiate the WP_Scripts object unless script names are explicitly passed. * Makes use of already-instantiated $wp_scripts global if present. Use provided {@see 'wp_print_scripts'} * hook to register/enqueue new scripts. * * @see WP_Scripts::do_item() * @global WP_Scripts $wp_scripts The WP_Scripts object for printing scripts. * * @since 2.1.0 * * @param string|bool|array $handles Optional. Scripts to be printed. Default 'false'. * @return string[] On success, an array of handles of processed WP_Dependencies items; otherwise, an empty array. */ function wp_print_scripts( $handles = false ) { global $wp_scripts; /** * Fires before scripts in the $handles queue are printed. * * @since 2.1.0 */ do_action( 'wp_print_scripts' ); if ( '' === $handles ) { // For 'wp_head'. $handles = false; } _wp_scripts_maybe_doing_it_wrong( __FUNCTION__ ); if ( ! ( $wp_scripts instanceof WP_Scripts ) ) { if ( ! $handles ) { return array(); // No need to instantiate if nothing is there. } } return wp_scripts()->do_items( $handles ); } /** * Adds extra code to a registered script. * * Code will only be added if the script is already in the queue. * Accepts a string $data containing the Code. If two or more code blocks * are added to the same script $handle, they will be printed in the order * they were added, i.e. the latter added code can redeclare the previous. * * @since 4.5.0 * * @see WP_Scripts::add_inline_script() * * @param string $handle Name of the script to add the inline script to. * @param string $data String containing the JavaScript to be added. * @param string $position Optional. Whether to add the inline script before the handle * or after. Default 'after'. * @return bool True on success, false on failure. */ function wp_add_inline_script( $handle, $data, $position = 'after' ) { _wp_scripts_maybe_doing_it_wrong( __FUNCTION__, $handle ); if ( false !== stripos( $data, '' ) ) { _doing_it_wrong( __FUNCTION__, sprintf( /* translators: 1: #is', '$1', $data ) ); } return wp_scripts()->add_inline_script( $handle, $data, $position ); } /** * Registers a new script. * * Registers a script to be enqueued later using the wp_enqueue_script() function. * * @see WP_Dependencies::add() * @see WP_Dependencies::add_data() * * @since 2.1.0 * @since 4.3.0 A return value was added. * * @param string $handle Name of the script. Should be unique. * @param string|false $src Full URL of the script, or path of the script relative to the WordPress root directory. * If source is set to false, script is an alias of other scripts it depends on. * @param string[] $deps Optional. An array of registered script handles this script depends on. Default empty array. * @param string|bool|null $ver Optional. String specifying script version number, if it has one, which is added to the URL * as a query string for cache busting purposes. If version is set to false, a version * number is automatically added equal to current installed WordPress version. * If set to null, no version is added. * @param bool $in_footer Optional. Whether to enqueue the script before `` instead of in the ``. * Default 'false'. * @return bool Whether the script has been registered. True on success, false on failure. */ function wp_register_script( $handle, $src, $deps = array(), $ver = false, $in_footer = false ) { _wp_scripts_maybe_doing_it_wrong( __FUNCTION__, $handle ); $wp_scripts = wp_scripts(); $registered = $wp_scripts->add( $handle, $src, $deps, $ver ); if ( $in_footer ) { $wp_scripts->add_data( $handle, 'group', 1 ); } return $registered; } /** * Localizes a script. * * Works only if the script has already been registered. * * Accepts an associative array $l10n and creates a JavaScript object: * * "$object_name" = { * key: value, * key: value, * ... * } * * @see WP_Scripts::localize() * @link https://core.trac.wordpress.org/ticket/11520 * @global WP_Scripts $wp_scripts The WP_Scripts object for printing scripts. * * @since 2.2.0 * * @todo Documentation cleanup * * @param string $handle Script handle the data will be attached to. * @param string $object_name Name for the JavaScript object. Passed directly, so it should be qualified JS variable. * Example: '/[a-zA-Z0-9_]+/'. * @param array $l10n The data itself. The data can be either a single or multi-dimensional array. * @return bool True if the script was successfully localized, false otherwise. */ function wp_localize_script( $handle, $object_name, $l10n ) { global $wp_scripts; if ( ! ( $wp_scripts instanceof WP_Scripts ) ) { _wp_scripts_maybe_doing_it_wrong( __FUNCTION__, $handle ); return false; } return $wp_scripts->localize( $handle, $object_name, $l10n ); } /** * Sets translated strings for a script. * * Works only if the script has already been registered. * * @see WP_Scripts::set_translations() * @global WP_Scripts $wp_scripts The WP_Scripts object for printing scripts. * * @since 5.0.0 * @since 5.1.0 The `$domain` parameter was made optional. * * @param string $handle Script handle the textdomain will be attached to. * @param string $domain Optional. Text domain. Default 'default'. * @param string $path Optional. The full file path to the directory containing translation files. * @return bool True if the text domain was successfully localized, false otherwise. */ function wp_set_script_translations( $handle, $domain = 'default', $path = '' ) { global $wp_scripts; if ( ! ( $wp_scripts instanceof WP_Scripts ) ) { _wp_scripts_maybe_doing_it_wrong( __FUNCTION__, $handle ); return false; } return $wp_scripts->set_translations( $handle, $domain, $path ); } /** * Removes a registered script. * * Note: there are intentional safeguards in place to prevent critical admin scripts, * such as jQuery core, from being unregistered. * * @see WP_Dependencies::remove() * * @since 2.1.0 * * @global string $pagenow The filename of the current screen. * * @param string $handle Name of the script to be removed. */ function wp_deregister_script( $handle ) { global $pagenow; _wp_scripts_maybe_doing_it_wrong( __FUNCTION__, $handle ); /** * Do not allow accidental or negligent de-registering of critical scripts in the admin. * Show minimal remorse if the correct hook is used. */ $current_filter = current_filter(); if ( ( is_admin() && 'admin_enqueue_scripts' !== $current_filter ) || ( 'wp-login.php' === $pagenow && 'login_enqueue_scripts' !== $current_filter ) ) { $not_allowed = array( 'jquery', 'jquery-core', 'jquery-migrate', 'jquery-ui-core', 'jquery-ui-accordion', 'jquery-ui-autocomplete', 'jquery-ui-button', 'jquery-ui-datepicker', 'jquery-ui-dialog', 'jquery-ui-draggable', 'jquery-ui-droppable', 'jquery-ui-menu', 'jquery-ui-mouse', 'jquery-ui-position', 'jquery-ui-progressbar', 'jquery-ui-resizable', 'jquery-ui-selectable', 'jquery-ui-slider', 'jquery-ui-sortable', 'jquery-ui-spinner', 'jquery-ui-tabs', 'jquery-ui-tooltip', 'jquery-ui-widget', 'underscore', 'backbone', ); if ( in_array( $handle, $not_allowed, true ) ) { _doing_it_wrong( __FUNCTION__, sprintf( /* translators: 1: Script name, 2: wp_enqueue_scripts */ __( 'Do not deregister the %1$s script in the administration area. To target the front-end theme, use the %2$s hook.' ), "$handle", 'wp_enqueue_scripts' ), '3.6.0' ); return; } } wp_scripts()->remove( $handle ); } /** * Enqueues a script. * * Registers the script if $src provided (does NOT overwrite), and enqueues it. * * @see WP_Dependencies::add() * @see WP_Dependencies::add_data() * @see WP_Dependencies::enqueue() * * @since 2.1.0 * * @param string $handle Name of the script. Should be unique. * @param string $src Full URL of the script, or path of the script relative to the WordPress root directory. * Default empty. * @param string[] $deps Optional. An array of registered script handles this script depends on. Default empty array. * @param string|bool|null $ver Optional. String specifying script version number, if it has one, which is added to the URL * as a query string for cache busting purposes. If version is set to false, a version * number is automatically added equal to current installed WordPress version. * If set to null, no version is added. * @param bool $in_footer Optional. Whether to enqueue the script before `` instead of in the ``. * Default 'false'. */ function wp_enqueue_script( $handle, $src = '', $deps = array(), $ver = false, $in_footer = false ) { _wp_scripts_maybe_doing_it_wrong( __FUNCTION__, $handle ); $wp_scripts = wp_scripts(); if ( $src || $in_footer ) { $_handle = explode( '?', $handle ); if ( $src ) { $wp_scripts->add( $_handle[0], $src, $deps, $ver ); } if ( $in_footer ) { $wp_scripts->add_data( $_handle[0], 'group', 1 ); } } $wp_scripts->enqueue( $handle ); } /** * Removes a previously enqueued script. * * @see WP_Dependencies::dequeue() * * @since 3.1.0 * * @param string $handle Name of the script to be removed. */ function wp_dequeue_script( $handle ) { _wp_scripts_maybe_doing_it_wrong( __FUNCTION__, $handle ); wp_scripts()->dequeue( $handle ); } /** * Determines whether a script has been added to the queue. * * 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.8.0 * @since 3.5.0 'enqueued' added as an alias of the 'queue' list. * * @param string $handle Name of the script. * @param string $status Optional. Status of the script to check. Default 'enqueued'. * Accepts 'enqueued', 'registered', 'queue', 'to_do', and 'done'. * @return bool Whether the script is queued. */ function wp_script_is( $handle, $status = 'enqueued' ) { _wp_scripts_maybe_doing_it_wrong( __FUNCTION__, $handle ); return (bool) wp_scripts()->query( $handle, $status ); } /** * Adds metadata to a script. * * Works only if the script has already been registered. * * Possible values for $key and $value: * 'conditional' string Comments for IE 6, lte IE 7, etc. * * @since 4.2.0 * * @see WP_Dependencies::add_data() * * @param string $handle Name of the script. * @param string $key Name of data point for which we're storing a value. * @param mixed $value String containing the data to be added. * @return bool True on success, false on failure. */ function wp_script_add_data( $handle, $key, $value ) { return wp_scripts()->add_data( $handle, $key, $value ); } /** * Taxonomy API: WP_Term class * * @package WordPress * @subpackage Taxonomy * @since 4.4.0 */ /** * Core class used to implement the WP_Term object. * * @since 4.4.0 * * @property-read object $data Sanitized term data. */ #[AllowDynamicProperties] final class WP_Term { /** * Term ID. * * @since 4.4.0 * @var int */ public $term_id; /** * The term's name. * * @since 4.4.0 * @var string */ public $name = ''; /** * The term's slug. * * @since 4.4.0 * @var string */ public $slug = ''; /** * The term's term_group. * * @since 4.4.0 * @var int */ public $term_group = ''; /** * Term Taxonomy ID. * * @since 4.4.0 * @var int */ public $term_taxonomy_id = 0; /** * The term's taxonomy name. * * @since 4.4.0 * @var string */ public $taxonomy = ''; /** * The term's description. * * @since 4.4.0 * @var string */ public $description = ''; /** * ID of a term's parent term. * * @since 4.4.0 * @var int */ public $parent = 0; /** * Cached object count for this term. * * @since 4.4.0 * @var int */ public $count = 0; /** * Stores the term object's sanitization level. * * Does not correspond to a database field. * * @since 4.4.0 * @var string */ public $filter = 'raw'; /** * Retrieve WP_Term instance. * * @since 4.4.0 * * @global wpdb $wpdb WordPress database abstraction object. * * @param int $term_id Term ID. * @param string $taxonomy Optional. Limit matched terms to those matching `$taxonomy`. Only used for * disambiguating potentially shared terms. * @return WP_Term|WP_Error|false Term object, if found. WP_Error if `$term_id` is shared between taxonomies and * there's insufficient data to distinguish which term is intended. * False for other failures. */ public static function get_instance( $term_id, $taxonomy = null ) { global $wpdb; $term_id = (int) $term_id; if ( ! $term_id ) { return false; } $_term = wp_cache_get( $term_id, 'terms' ); // If there isn't a cached version, hit the database. if ( ! $_term || ( $taxonomy && $taxonomy !== $_term->taxonomy ) ) { // Any term found in the cache is not a match, so don't use it. $_term = false; // Grab all matching terms, in case any are shared between taxonomies. $terms = $wpdb->get_results( $wpdb->prepare( "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 = %d", $term_id ) ); if ( ! $terms ) { return false; } // If a taxonomy was specified, find a match. if ( $taxonomy ) { foreach ( $terms as $match ) { if ( $taxonomy === $match->taxonomy ) { $_term = $match; break; } } // If only one match was found, it's the one we want. } elseif ( 1 === count( $terms ) ) { $_term = reset( $terms ); // Otherwise, the term must be shared between taxonomies. } else { // If the term is shared only with invalid taxonomies, return the one valid term. foreach ( $terms as $t ) { if ( ! taxonomy_exists( $t->taxonomy ) ) { continue; } // Only hit if we've already identified a term in a valid taxonomy. if ( $_term ) { return new WP_Error( 'ambiguous_term_id', __( 'Term ID is shared between multiple taxonomies' ), $term_id ); } $_term = $t; } } if ( ! $_term ) { return false; } // Don't return terms from invalid taxonomies. if ( ! taxonomy_exists( $_term->taxonomy ) ) { return new WP_Error( 'invalid_taxonomy', __( 'Invalid taxonomy.' ) ); } $_term = sanitize_term( $_term, $_term->taxonomy, 'raw' ); // Don't cache terms that are shared between taxonomies. if ( 1 === count( $terms ) ) { wp_cache_add( $term_id, $_term, 'terms' ); } } $term_obj = new WP_Term( $_term ); $term_obj->filter( $term_obj->filter ); return $term_obj; } /** * Constructor. * * @since 4.4.0 * * @param WP_Term|object $term Term object. */ public function __construct( $term ) { foreach ( get_object_vars( $term ) as $key => $value ) { $this->$key = $value; } } /** * Sanitizes term fields, according to the filter type provided. * * @since 4.4.0 * * @param string $filter Filter context. Accepts 'edit', 'db', 'display', 'attribute', 'js', 'rss', or 'raw'. */ public function filter( $filter ) { sanitize_term( $this, $this->taxonomy, $filter ); } /** * Converts an object to array. * * @since 4.4.0 * * @return array Object as array. */ public function to_array() { return get_object_vars( $this ); } /** * Getter. * * @since 4.4.0 * * @param string $key Property to get. * @return mixed Property value. */ public function __get( $key ) { switch ( $key ) { case 'data': $data = new stdClass(); $columns = array( 'term_id', 'name', 'slug', 'term_group', 'term_taxonomy_id', 'taxonomy', 'description', 'parent', 'count' ); foreach ( $columns as $column ) { $data->{$column} = isset( $this->{$column} ) ? $this->{$column} : null; } return sanitize_term( $data, $data->taxonomy, 'raw' ); } } } /** * Widget API: WP_Widget base class * * @package WordPress * @subpackage Widgets * @since 4.4.0 */ /** * Core base class extended to register widgets. * * This class must be extended for each widget, and WP_Widget::widget() must be overridden. * * If adding widget options, WP_Widget::update() and WP_Widget::form() should also be overridden. * * @since 2.8.0 * @since 4.4.0 Moved to its own file from wp-includes/widgets.php */ #[AllowDynamicProperties] class WP_Widget { /** * Root ID for all widgets of this type. * * @since 2.8.0 * @var mixed|string */ public $id_base; /** * Name for this widget type. * * @since 2.8.0 * @var string */ public $name; /** * Option name for this widget type. * * @since 2.8.0 * @var string */ public $option_name; /** * Alt option name for this widget type. * * @since 2.8.0 * @var string */ public $alt_option_name; /** * Option array passed to wp_register_sidebar_widget(). * * @since 2.8.0 * @var array */ public $widget_options; /** * Option array passed to wp_register_widget_control(). * * @since 2.8.0 * @var array */ public $control_options; /** * Unique ID number of the current instance. * * @since 2.8.0 * @var bool|int */ public $number = false; /** * Unique ID string of the current instance (id_base-number). * * @since 2.8.0 * @var bool|string */ public $id = false; /** * Whether the widget data has been updated. * * Set to true when the data is updated after a POST submit - ensures it does * not happen twice. * * @since 2.8.0 * @var bool */ public $updated = false; // // Member functions that must be overridden by subclasses. // /** * Echoes the widget content. * * Subclasses should override this function to generate their widget code. * * @since 2.8.0 * * @param array $args Display arguments including 'before_title', 'after_title', * 'before_widget', and 'after_widget'. * @param array $instance The settings for the particular instance of the widget. */ public function widget( $args, $instance ) { die( 'function WP_Widget::widget() must be overridden in a subclass.' ); } /** * Updates a particular instance of a widget. * * This function should check that `$new_instance` is set correctly. The newly-calculated * value of `$instance` should be returned. If false is returned, the instance won't be * saved/updated. * * @since 2.8.0 * * @param array $new_instance New settings for this instance as input by the user via * WP_Widget::form(). * @param array $old_instance Old settings for this instance. * @return array Settings to save or bool false to cancel saving. */ public function update( $new_instance, $old_instance ) { return $new_instance; } /** * Outputs the settings update form. * * @since 2.8.0 * * @param array $instance Current settings. * @return string Default return is 'noform'. */ public function form( $instance ) { echo '

' . __( 'There are no options for this widget.' ) . '

'; return 'noform'; } // Functions you'll need to call. /** * PHP5 constructor. * * @since 2.8.0 * * @param string $id_base Base ID for the widget, lowercase and unique. If left empty, * a portion of the widget's PHP class name will be used. Has to be unique. * @param string $name Name for the widget displayed on the configuration page. * @param array $widget_options Optional. Widget options. See wp_register_sidebar_widget() for * information on accepted arguments. Default empty array. * @param array $control_options Optional. Widget control options. See wp_register_widget_control() for * information on accepted arguments. Default empty array. */ public function __construct( $id_base, $name, $widget_options = array(), $control_options = array() ) { if ( ! empty( $id_base ) ) { $id_base = strtolower( $id_base ); } else { $id_base = preg_replace( '/(wp_)?widget_/', '', strtolower( get_class( $this ) ) ); } $this->id_base = $id_base; $this->name = $name; $this->option_name = 'widget_' . $this->id_base; $this->widget_options = wp_parse_args( $widget_options, array( 'classname' => str_replace( '\\', '_', $this->option_name ), 'customize_selective_refresh' => false, ) ); $this->control_options = wp_parse_args( $control_options, array( 'id_base' => $this->id_base ) ); } /** * PHP4 constructor. * * @since 2.8.0 * @deprecated 4.3.0 Use __construct() instead. * * @see WP_Widget::__construct() * * @param string $id_base Base ID for the widget, lowercase and unique. If left empty, * a portion of the widget's PHP class name will be used. Has to be unique. * @param string $name Name for the widget displayed on the configuration page. * @param array $widget_options Optional. Widget options. See wp_register_sidebar_widget() for * information on accepted arguments. Default empty array. * @param array $control_options Optional. Widget control options. See wp_register_widget_control() for * information on accepted arguments. Default empty array. */ public function WP_Widget( $id_base, $name, $widget_options = array(), $control_options = array() ) { _deprecated_constructor( 'WP_Widget', '4.3.0', get_class( $this ) ); WP_Widget::__construct( $id_base, $name, $widget_options, $control_options ); } /** * Constructs name attributes for use in form() fields * * This function should be used in form() methods to create name attributes for fields * to be saved by update() * * @since 2.8.0 * @since 4.4.0 Array format field names are now accepted. * * @param string $field_name Field name. * @return string Name attribute for `$field_name`. */ public function get_field_name( $field_name ) { $pos = strpos( $field_name, '[' ); if ( false !== $pos ) { // Replace the first occurrence of '[' with ']['. $field_name = '[' . substr_replace( $field_name, '][', $pos, strlen( '[' ) ); } else { $field_name = '[' . $field_name . ']'; } return 'widget-' . $this->id_base . '[' . $this->number . ']' . $field_name; } /** * Constructs id attributes for use in WP_Widget::form() fields. * * This function should be used in form() methods to create id attributes * for fields to be saved by WP_Widget::update(). * * @since 2.8.0 * @since 4.4.0 Array format field IDs are now accepted. * * @param string $field_name Field name. * @return string ID attribute for `$field_name`. */ public function get_field_id( $field_name ) { $field_name = str_replace( array( '[]', '[', ']' ), array( '', '-', '' ), $field_name ); $field_name = trim( $field_name, '-' ); return 'widget-' . $this->id_base . '-' . $this->number . '-' . $field_name; } /** * Register all widget instances of this widget class. * * @since 2.8.0 */ public function _register() { $settings = $this->get_settings(); $empty = true; // When $settings is an array-like object, get an intrinsic array for use with array_keys(). if ( $settings instanceof ArrayObject || $settings instanceof ArrayIterator ) { $settings = $settings->getArrayCopy(); } if ( is_array( $settings ) ) { foreach ( array_keys( $settings ) as $number ) { if ( is_numeric( $number ) ) { $this->_set( $number ); $this->_register_one( $number ); $empty = false; } } } if ( $empty ) { // If there are none, we register the widget's existence with a generic template. $this->_set( 1 ); $this->_register_one(); } } /** * Sets the internal order number for the widget instance. * * @since 2.8.0 * * @param int $number The unique order number of this widget instance compared to other * instances of the same class. */ public function _set( $number ) { $this->number = $number; $this->id = $this->id_base . '-' . $number; } /** * Retrieves the widget display callback. * * @since 2.8.0 * * @return callable Display callback. */ public function _get_display_callback() { return array( $this, 'display_callback' ); } /** * Retrieves the widget update callback. * * @since 2.8.0 * * @return callable Update callback. */ public function _get_update_callback() { return array( $this, 'update_callback' ); } /** * Retrieves the form callback. * * @since 2.8.0 * * @return callable Form callback. */ public function _get_form_callback() { return array( $this, 'form_callback' ); } /** * Determines whether the current request is inside the Customizer preview. * * If true -- the current request is inside the Customizer preview, then * the object cache gets suspended and widgets should check this to decide * whether they should store anything persistently to the object cache, * to transients, or anywhere else. * * @since 3.9.0 * * @global WP_Customize_Manager $wp_customize * * @return bool True if within the Customizer preview, false if not. */ public function is_preview() { global $wp_customize; return ( isset( $wp_customize ) && $wp_customize->is_preview() ); } /** * Generates the actual widget content (Do NOT override). * * Finds the instance and calls WP_Widget::widget(). * * @since 2.8.0 * * @param array $args Display arguments. See WP_Widget::widget() for information * on accepted arguments. * @param int|array $widget_args { * Optional. Internal order number of the widget instance, or array of multi-widget arguments. * Default 1. * * @type int $number Number increment used for multiples of the same widget. * } */ public function display_callback( $args, $widget_args = 1 ) { if ( is_numeric( $widget_args ) ) { $widget_args = array( 'number' => $widget_args ); } $widget_args = wp_parse_args( $widget_args, array( 'number' => -1 ) ); $this->_set( $widget_args['number'] ); $instances = $this->get_settings(); if ( isset( $instances[ $this->number ] ) ) { $instance = $instances[ $this->number ]; /** * Filters the settings for a particular widget instance. * * Returning false will effectively short-circuit display of the widget. * * @since 2.8.0 * * @param array $instance The current widget instance's settings. * @param WP_Widget $widget The current widget instance. * @param array $args An array of default widget arguments. */ $instance = apply_filters( 'widget_display_callback', $instance, $this, $args ); if ( false === $instance ) { return; } $was_cache_addition_suspended = wp_suspend_cache_addition(); if ( $this->is_preview() && ! $was_cache_addition_suspended ) { wp_suspend_cache_addition( true ); } $this->widget( $args, $instance ); if ( $this->is_preview() ) { wp_suspend_cache_addition( $was_cache_addition_suspended ); } } } /** * Handles changed settings (Do NOT override). * * @since 2.8.0 * * @global array $wp_registered_widgets * * @param int $deprecated Not used. */ public function update_callback( $deprecated = 1 ) { global $wp_registered_widgets; $all_instances = $this->get_settings(); // We need to update the data. if ( $this->updated ) { return; } if ( isset( $_POST['delete_widget'] ) && $_POST['delete_widget'] ) { // Delete the settings for this instance of the widget. if ( isset( $_POST['the-widget-id'] ) ) { $del_id = $_POST['the-widget-id']; } else { return; } if ( isset( $wp_registered_widgets[ $del_id ]['params'][0]['number'] ) ) { $number = $wp_registered_widgets[ $del_id ]['params'][0]['number']; if ( $this->id_base . '-' . $number == $del_id ) { unset( $all_instances[ $number ] ); } } } else { if ( isset( $_POST[ 'widget-' . $this->id_base ] ) && is_array( $_POST[ 'widget-' . $this->id_base ] ) ) { $settings = $_POST[ 'widget-' . $this->id_base ]; } elseif ( isset( $_POST['id_base'] ) && $_POST['id_base'] == $this->id_base ) { $num = $_POST['multi_number'] ? (int) $_POST['multi_number'] : (int) $_POST['widget_number']; $settings = array( $num => array() ); } else { return; } foreach ( $settings as $number => $new_instance ) { $new_instance = stripslashes_deep( $new_instance ); $this->_set( $number ); $old_instance = isset( $all_instances[ $number ] ) ? $all_instances[ $number ] : array(); $was_cache_addition_suspended = wp_suspend_cache_addition(); if ( $this->is_preview() && ! $was_cache_addition_suspended ) { wp_suspend_cache_addition( true ); } $instance = $this->update( $new_instance, $old_instance ); if ( $this->is_preview() ) { wp_suspend_cache_addition( $was_cache_addition_suspended ); } /** * Filters a widget's settings before saving. * * Returning false will effectively short-circuit the widget's ability * to update settings. * * @since 2.8.0 * * @param array $instance The current widget instance's settings. * @param array $new_instance Array of new widget settings. * @param array $old_instance Array of old widget settings. * @param WP_Widget $widget The current widget instance. */ $instance = apply_filters( 'widget_update_callback', $instance, $new_instance, $old_instance, $this ); if ( false !== $instance ) { $all_instances[ $number ] = $instance; } break; // Run only once. } } $this->save_settings( $all_instances ); $this->updated = true; } /** * Generates the widget control form (Do NOT override). * * @since 2.8.0 * * @param int|array $widget_args { * Optional. Internal order number of the widget instance, or array of multi-widget arguments. * Default 1. * * @type int $number Number increment used for multiples of the same widget. * } * @return string|null */ public function form_callback( $widget_args = 1 ) { if ( is_numeric( $widget_args ) ) { $widget_args = array( 'number' => $widget_args ); } $widget_args = wp_parse_args( $widget_args, array( 'number' => -1 ) ); $all_instances = $this->get_settings(); if ( -1 == $widget_args['number'] ) { // We echo out a form where 'number' can be set later. $this->_set( '__i__' ); $instance = array(); } else { $this->_set( $widget_args['number'] ); $instance = $all_instances[ $widget_args['number'] ]; } /** * Filters the widget instance's settings before displaying the control form. * * Returning false effectively short-circuits display of the control form. * * @since 2.8.0 * * @param array $instance The current widget instance's settings. * @param WP_Widget $widget The current widget instance. */ $instance = apply_filters( 'widget_form_callback', $instance, $this ); $return = null; if ( false !== $instance ) { $return = $this->form( $instance ); /** * Fires at the end of the widget control form. * * Use this hook to add extra fields to the widget form. The hook * is only fired if the value passed to the 'widget_form_callback' * hook is not false. * * Note: If the widget has no form, the text echoed from the default * form method can be hidden using CSS. * * @since 2.8.0 * * @param WP_Widget $widget The widget instance (passed by reference). * @param null $return Return null if new fields are added. * @param array $instance An array of the widget's settings. */ do_action_ref_array( 'in_widget_form', array( &$this, &$return, $instance ) ); } return $return; } /** * Registers an instance of the widget class. * * @since 2.8.0 * * @param int $number Optional. The unique order number of this widget instance * compared to other instances of the same class. Default -1. */ public function _register_one( $number = -1 ) { wp_register_sidebar_widget( $this->id, $this->name, $this->_get_display_callback(), $this->widget_options, array( 'number' => $number ) ); _register_widget_update_callback( $this->id_base, $this->_get_update_callback(), $this->control_options, array( 'number' => -1 ) ); _register_widget_form_callback( $this->id, $this->name, $this->_get_form_callback(), $this->control_options, array( 'number' => $number ) ); } /** * Saves the settings for all instances of the widget class. * * @since 2.8.0 * * @param array $settings Multi-dimensional array of widget instance settings. */ public function save_settings( $settings ) { $settings['_multiwidget'] = 1; update_option( $this->option_name, $settings ); } /** * Retrieves the settings for all instances of the widget class. * * @since 2.8.0 * * @return array Multi-dimensional array of widget instance settings. */ public function get_settings() { $settings = get_option( $this->option_name ); if ( false === $settings ) { $settings = array(); if ( isset( $this->alt_option_name ) ) { // Get settings from alternative (legacy) option. $settings = get_option( $this->alt_option_name, array() ); // Delete the alternative (legacy) option as the new option will be created using `$this->option_name`. delete_option( $this->alt_option_name ); } // Save an option so it can be autoloaded next time. $this->save_settings( $settings ); } if ( ! is_array( $settings ) && ! ( $settings instanceof ArrayObject || $settings instanceof ArrayIterator ) ) { $settings = array(); } if ( ! empty( $settings ) && ! isset( $settings['_multiwidget'] ) ) { // Old format, convert if single widget. $settings = wp_convert_widget_settings( $this->id_base, $this->option_name, $settings ); } unset( $settings['_multiwidget'], $settings['__i__'] ); return $settings; } } /** * REST API: WP_REST_Response class * * @package WordPress * @subpackage REST_API * @since 4.4.0 */ /** * Core class used to implement a REST response object. * * @since 4.4.0 * * @see WP_HTTP_Response */ class WP_REST_Response extends WP_HTTP_Response { /** * Links related to the response. * * @since 4.4.0 * @var array */ protected $links = array(); /** * The route that was to create the response. * * @since 4.4.0 * @var string */ protected $matched_route = ''; /** * The handler that was used to create the response. * * @since 4.4.0 * @var null|array */ protected $matched_handler = null; /** * Adds a link to the response. * * @internal The $rel parameter is first, as this looks nicer when sending multiple. * * @since 4.4.0 * * @link https://tools.ietf.org/html/rfc5988 * @link https://www.iana.org/assignments/link-relations/link-relations.xml * * @param string $rel Link relation. Either an IANA registered type, * or an absolute URL. * @param string $href Target URI for the link. * @param array $attributes Optional. Link parameters to send along with the URL. Default empty array. */ public function add_link( $rel, $href, $attributes = array() ) { if ( empty( $this->links[ $rel ] ) ) { $this->links[ $rel ] = array(); } if ( isset( $attributes['href'] ) ) { // Remove the href attribute, as it's used for the main URL. unset( $attributes['href'] ); } $this->links[ $rel ][] = array( 'href' => $href, 'attributes' => $attributes, ); } /** * Removes a link from the response. * * @since 4.4.0 * * @param string $rel Link relation. Either an IANA registered type, or an absolute URL. * @param string $href Optional. Only remove links for the relation matching the given href. * Default null. */ public function remove_link( $rel, $href = null ) { if ( ! isset( $this->links[ $rel ] ) ) { return; } if ( $href ) { $this->links[ $rel ] = wp_list_filter( $this->links[ $rel ], array( 'href' => $href ), 'NOT' ); } else { $this->links[ $rel ] = array(); } if ( ! $this->links[ $rel ] ) { unset( $this->links[ $rel ] ); } } /** * Adds multiple links to the response. * * Link data should be an associative array with link relation as the key. * The value can either be an associative array of link attributes * (including `href` with the URL for the response), or a list of these * associative arrays. * * @since 4.4.0 * * @param array $links Map of link relation to list of links. */ public function add_links( $links ) { foreach ( $links as $rel => $set ) { // If it's a single link, wrap with an array for consistent handling. if ( isset( $set['href'] ) ) { $set = array( $set ); } foreach ( $set as $attributes ) { $this->add_link( $rel, $attributes['href'], $attributes ); } } } /** * Retrieves links for the response. * * @since 4.4.0 * * @return array List of links. */ public function get_links() { return $this->links; } /** * Sets a single link header. * * @internal The $rel parameter is first, as this looks nicer when sending multiple. * * @since 4.4.0 * * @link https://tools.ietf.org/html/rfc5988 * @link https://www.iana.org/assignments/link-relations/link-relations.xml * * @param string $rel Link relation. Either an IANA registered type, or an absolute URL. * @param string $link Target IRI for the link. * @param array $other Optional. Other parameters to send, as an associative array. * Default empty array. */ public function link_header( $rel, $link, $other = array() ) { $header = '<' . $link . '>; rel="' . $rel . '"'; foreach ( $other as $key => $value ) { if ( 'title' === $key ) { $value = '"' . $value . '"'; } $header .= '; ' . $key . '=' . $value; } $this->header( 'Link', $header, false ); } /** * Retrieves the route that was used. * * @since 4.4.0 * * @return string The matched route. */ public function get_matched_route() { return $this->matched_route; } /** * Sets the route (regex for path) that caused the response. * * @since 4.4.0 * * @param string $route Route name. */ public function set_matched_route( $route ) { $this->matched_route = $route; } /** * Retrieves the handler that was used to generate the response. * * @since 4.4.0 * * @return null|array The handler that was used to create the response. */ public function get_matched_handler() { return $this->matched_handler; } /** * Sets the handler that was responsible for generating the response. * * @since 4.4.0 * * @param array $handler The matched handler. */ public function set_matched_handler( $handler ) { $this->matched_handler = $handler; } /** * Checks if the response is an error, i.e. >= 400 response code. * * @since 4.4.0 * * @return bool Whether the response is an error. */ public function is_error() { return $this->get_status() >= 400; } /** * Retrieves a WP_Error object from the response. * * @since 4.4.0 * * @return WP_Error|null WP_Error or null on not an errored response. */ public function as_error() { if ( ! $this->is_error() ) { return null; } $error = new WP_Error(); if ( is_array( $this->get_data() ) ) { $data = $this->get_data(); $error->add( $data['code'], $data['message'], $data['data'] ); if ( ! empty( $data['additional_errors'] ) ) { foreach ( $data['additional_errors'] as $err ) { $error->add( $err['code'], $err['message'], $err['data'] ); } } } else { $error->add( $this->get_status(), '', array( 'status' => $this->get_status() ) ); } return $error; } /** * Retrieves the CURIEs (compact URIs) used for relations. * * @since 4.5.0 * * @return array Compact URIs. */ public function get_curies() { $curies = array( array( 'name' => 'wp', 'href' => 'https://api.w.org/{rel}', 'templated' => true, ), ); /** * Filters extra CURIEs available on REST API responses. * * CURIEs allow a shortened version of URI relations. This allows a more * usable form for custom relations than using the full URI. These work * similarly to how XML namespaces work. * * Registered CURIES need to specify a name and URI template. This will * automatically transform URI relations into their shortened version. * The shortened relation follows the format `{name}:{rel}`. `{rel}` in * the URI template will be replaced with the `{rel}` part of the * shortened relation. * * For example, a CURIE with name `example` and URI template * `http://w.org/{rel}` would transform a `http://w.org/term` relation * into `example:term`. * * Well-behaved clients should expand and normalize these back to their * full URI relation, however some naive clients may not resolve these * correctly, so adding new CURIEs may break backward compatibility. * * @since 4.5.0 * * @param array $additional Additional CURIEs to register with the REST API. */ $additional = apply_filters( 'rest_response_link_curies', array() ); return array_merge( $curies, $additional ); } } /** * REST API: WP_REST_Users_Controller class * * @package WordPress * @subpackage REST_API * @since 4.7.0 */ /** * Core class used to manage users via the REST API. * * @since 4.7.0 * * @see WP_REST_Controller */ class WP_REST_Users_Controller extends WP_REST_Controller { /** * Instance of a user meta fields object. * * @since 4.7.0 * @var WP_REST_User_Meta_Fields */ protected $meta; /** * Constructor. * * @since 4.7.0 */ public function __construct() { $this->namespace = 'wp/v2'; $this->rest_base = 'users'; $this->meta = new WP_REST_User_Meta_Fields(); } /** * Registers the routes for users. * * @since 4.7.0 * * @see register_rest_route() */ public function register_routes() { register_rest_route( $this->namespace, '/' . $this->rest_base, array( array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_items' ), 'permission_callback' => array( $this, 'get_items_permissions_check' ), 'args' => $this->get_collection_params(), ), array( 'methods' => WP_REST_Server::CREATABLE, 'callback' => array( $this, 'create_item' ), 'permission_callback' => array( $this, 'create_item_permissions_check' ), 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), ), 'schema' => array( $this, 'get_public_item_schema' ), ) ); register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P[\d]+)', array( 'args' => array( 'id' => array( 'description' => __( 'Unique identifier for the user.' ), 'type' => 'integer', ), ), array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_item' ), 'permission_callback' => array( $this, 'get_item_permissions_check' ), 'args' => array( 'context' => $this->get_context_param( array( 'default' => 'view' ) ), ), ), array( 'methods' => WP_REST_Server::EDITABLE, 'callback' => array( $this, 'update_item' ), 'permission_callback' => array( $this, 'update_item_permissions_check' ), 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), ), array( 'methods' => WP_REST_Server::DELETABLE, 'callback' => array( $this, 'delete_item' ), 'permission_callback' => array( $this, 'delete_item_permissions_check' ), 'args' => array( 'force' => array( 'type' => 'boolean', 'default' => false, 'description' => __( 'Required to be true, as users do not support trashing.' ), ), 'reassign' => array( 'type' => 'integer', 'description' => __( 'Reassign the deleted user\'s posts and links to this user ID.' ), 'required' => true, 'sanitize_callback' => array( $this, 'check_reassign' ), ), ), ), 'schema' => array( $this, 'get_public_item_schema' ), ) ); register_rest_route( $this->namespace, '/' . $this->rest_base . '/me', array( array( 'methods' => WP_REST_Server::READABLE, 'permission_callback' => '__return_true', 'callback' => array( $this, 'get_current_item' ), 'args' => array( 'context' => $this->get_context_param( array( 'default' => 'view' ) ), ), ), array( 'methods' => WP_REST_Server::EDITABLE, 'callback' => array( $this, 'update_current_item' ), 'permission_callback' => array( $this, 'update_current_item_permissions_check' ), 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), ), array( 'methods' => WP_REST_Server::DELETABLE, 'callback' => array( $this, 'delete_current_item' ), 'permission_callback' => array( $this, 'delete_current_item_permissions_check' ), 'args' => array( 'force' => array( 'type' => 'boolean', 'default' => false, 'description' => __( 'Required to be true, as users do not support trashing.' ), ), 'reassign' => array( 'type' => 'integer', 'description' => __( 'Reassign the deleted user\'s posts and links to this user ID.' ), 'required' => true, 'sanitize_callback' => array( $this, 'check_reassign' ), ), ), ), 'schema' => array( $this, 'get_public_item_schema' ), ) ); } /** * Checks for a valid value for the reassign parameter when deleting users. * * The value can be an integer, 'false', false, or ''. * * @since 4.7.0 * * @param int|bool $value The value passed to the reassign parameter. * @param WP_REST_Request $request Full details about the request. * @param string $param The parameter that is being sanitized. * @return int|bool|WP_Error */ public function check_reassign( $value, $request, $param ) { if ( is_numeric( $value ) ) { return $value; } if ( empty( $value ) || false === $value || 'false' === $value ) { return false; } return new WP_Error( 'rest_invalid_param', __( 'Invalid user parameter(s).' ), array( 'status' => 400 ) ); } /** * Permissions check for getting all users. * * @since 4.7.0 * * @param WP_REST_Request $request Full details about the request. * @return true|WP_Error True if the request has read access, otherwise WP_Error object. */ public function get_items_permissions_check( $request ) { // Check if roles is specified in GET request and if user can list users. if ( ! empty( $request['roles'] ) && ! current_user_can( 'list_users' ) ) { return new WP_Error( 'rest_user_cannot_view', __( 'Sorry, you are not allowed to filter users by role.' ), array( 'status' => rest_authorization_required_code() ) ); } // Check if capabilities is specified in GET request and if user can list users. if ( ! empty( $request['capabilities'] ) && ! current_user_can( 'list_users' ) ) { return new WP_Error( 'rest_user_cannot_view', __( 'Sorry, you are not allowed to filter users by capability.' ), array( 'status' => rest_authorization_required_code() ) ); } if ( 'edit' === $request['context'] && ! current_user_can( 'list_users' ) ) { return new WP_Error( 'rest_forbidden_context', __( 'Sorry, you are not allowed to list users.' ), array( 'status' => rest_authorization_required_code() ) ); } if ( in_array( $request['orderby'], array( 'email', 'registered_date' ), true ) && ! current_user_can( 'list_users' ) ) { return new WP_Error( 'rest_forbidden_orderby', __( 'Sorry, you are not allowed to order users by this parameter.' ), array( 'status' => rest_authorization_required_code() ) ); } if ( 'authors' === $request['who'] ) { $types = get_post_types( array( 'show_in_rest' => true ), 'objects' ); foreach ( $types as $type ) { if ( post_type_supports( $type->name, 'author' ) && current_user_can( $type->cap->edit_posts ) ) { return true; } } return new WP_Error( 'rest_forbidden_who', __( 'Sorry, you are not allowed to query users by this parameter.' ), array( 'status' => rest_authorization_required_code() ) ); } return true; } /** * Retrieves all users. * * @since 4.7.0 * * @param WP_REST_Request $request Full details about the request. * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. */ public function get_items( $request ) { // Retrieve the list of registered collection query parameters. $registered = $this->get_collection_params(); /* * This array defines mappings between public API query parameters whose * values are accepted as-passed, and their internal WP_Query parameter * name equivalents (some are the same). Only values which are also * present in $registered will be set. */ $parameter_mappings = array( 'exclude' => 'exclude', 'include' => 'include', 'order' => 'order', 'per_page' => 'number', 'search' => 'search', 'roles' => 'role__in', 'capabilities' => 'capability__in', 'slug' => 'nicename__in', ); $prepared_args = array(); /* * For each known parameter which is both registered and present in the request, * set the parameter's value on the query $prepared_args. */ foreach ( $parameter_mappings as $api_param => $wp_param ) { if ( isset( $registered[ $api_param ], $request[ $api_param ] ) ) { $prepared_args[ $wp_param ] = $request[ $api_param ]; } } if ( isset( $registered['offset'] ) && ! empty( $request['offset'] ) ) { $prepared_args['offset'] = $request['offset']; } else { $prepared_args['offset'] = ( $request['page'] - 1 ) * $prepared_args['number']; } if ( isset( $registered['orderby'] ) ) { $orderby_possibles = array( 'id' => 'ID', 'include' => 'include', 'name' => 'display_name', 'registered_date' => 'registered', 'slug' => 'user_nicename', 'include_slugs' => 'nicename__in', 'email' => 'user_email', 'url' => 'user_url', ); $prepared_args['orderby'] = $orderby_possibles[ $request['orderby'] ]; } if ( isset( $registered['who'] ) && ! empty( $request['who'] ) && 'authors' === $request['who'] ) { $prepared_args['who'] = 'authors'; } elseif ( ! current_user_can( 'list_users' ) ) { $prepared_args['has_published_posts'] = get_post_types( array( 'show_in_rest' => true ), 'names' ); } if ( ! empty( $request['has_published_posts'] ) ) { $prepared_args['has_published_posts'] = ( true === $request['has_published_posts'] ) ? get_post_types( array( 'show_in_rest' => true ), 'names' ) : (array) $request['has_published_posts']; } if ( ! empty( $prepared_args['search'] ) ) { $prepared_args['search'] = '*' . $prepared_args['search'] . '*'; } /** * Filters WP_User_Query arguments when querying users via the REST API. * * @link https://developer.wordpress.org/reference/classes/wp_user_query/ * * @since 4.7.0 * * @param array $prepared_args Array of arguments for WP_User_Query. * @param WP_REST_Request $request The REST API request. */ $prepared_args = apply_filters( 'rest_user_query', $prepared_args, $request ); $query = new WP_User_Query( $prepared_args ); $users = array(); foreach ( $query->results as $user ) { $data = $this->prepare_item_for_response( $user, $request ); $users[] = $this->prepare_response_for_collection( $data ); } $response = rest_ensure_response( $users ); // Store pagination values for headers then unset for count query. $per_page = (int) $prepared_args['number']; $page = ceil( ( ( (int) $prepared_args['offset'] ) / $per_page ) + 1 ); $prepared_args['fields'] = 'ID'; $total_users = $query->get_total(); if ( $total_users < 1 ) { // Out-of-bounds, run the query again without LIMIT for total count. unset( $prepared_args['number'], $prepared_args['offset'] ); $count_query = new WP_User_Query( $prepared_args ); $total_users = $count_query->get_total(); } $response->header( 'X-WP-Total', (int) $total_users ); $max_pages = ceil( $total_users / $per_page ); $response->header( 'X-WP-TotalPages', (int) $max_pages ); $base = add_query_arg( urlencode_deep( $request->get_query_params() ), rest_url( sprintf( '%s/%s', $this->namespace, $this->rest_base ) ) ); if ( $page > 1 ) { $prev_page = $page - 1; if ( $prev_page > $max_pages ) { $prev_page = $max_pages; } $prev_link = add_query_arg( 'page', $prev_page, $base ); $response->link_header( 'prev', $prev_link ); } if ( $max_pages > $page ) { $next_page = $page + 1; $next_link = add_query_arg( 'page', $next_page, $base ); $response->link_header( 'next', $next_link ); } return $response; } /** * Get the user, if the ID is valid. * * @since 4.7.2 * * @param int $id Supplied ID. * @return WP_User|WP_Error True if ID is valid, WP_Error otherwise. */ protected function get_user( $id ) { $error = new WP_Error( 'rest_user_invalid_id', __( 'Invalid user ID.' ), array( 'status' => 404 ) ); if ( (int) $id <= 0 ) { return $error; } $user = get_userdata( (int) $id ); if ( empty( $user ) || ! $user->exists() ) { return $error; } if ( is_multisite() && ! is_user_member_of_blog( $user->ID ) ) { return $error; } return $user; } /** * Checks if a given request has access to read a user. * * @since 4.7.0 * * @param WP_REST_Request $request Full details about the request. * @return true|WP_Error True if the request has read access for the item, otherwise WP_Error object. */ public function get_item_permissions_check( $request ) { $user = $this->get_user( $request['id'] ); if ( is_wp_error( $user ) ) { return $user; } $types = get_post_types( array( 'show_in_rest' => true ), 'names' ); if ( get_current_user_id() === $user->ID ) { return true; } if ( 'edit' === $request['context'] && ! current_user_can( 'list_users' ) ) { return new WP_Error( 'rest_user_cannot_view', __( 'Sorry, you are not allowed to list users.' ), array( 'status' => rest_authorization_required_code() ) ); } elseif ( ! count_user_posts( $user->ID, $types ) && ! current_user_can( 'edit_user', $user->ID ) && ! current_user_can( 'list_users' ) ) { return new WP_Error( 'rest_user_cannot_view', __( 'Sorry, you are not allowed to list users.' ), array( 'status' => rest_authorization_required_code() ) ); } return true; } /** * Retrieves a single user. * * @since 4.7.0 * * @param WP_REST_Request $request Full details about the request. * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. */ public function get_item( $request ) { $user = $this->get_user( $request['id'] ); if ( is_wp_error( $user ) ) { return $user; } $user = $this->prepare_item_for_response( $user, $request ); $response = rest_ensure_response( $user ); return $response; } /** * Retrieves the current user. * * @since 4.7.0 * * @param WP_REST_Request $request Full details about the request. * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. */ public function get_current_item( $request ) { $current_user_id = get_current_user_id(); if ( empty( $current_user_id ) ) { return new WP_Error( 'rest_not_logged_in', __( 'You are not currently logged in.' ), array( 'status' => 401 ) ); } $user = wp_get_current_user(); $response = $this->prepare_item_for_response( $user, $request ); $response = rest_ensure_response( $response ); return $response; } /** * Checks if a given request has access create users. * * @since 4.7.0 * * @param WP_REST_Request $request Full details about the request. * @return true|WP_Error True if the request has access to create items, WP_Error object otherwise. */ public function create_item_permissions_check( $request ) { if ( ! current_user_can( 'create_users' ) ) { return new WP_Error( 'rest_cannot_create_user', __( 'Sorry, you are not allowed to create new users.' ), array( 'status' => rest_authorization_required_code() ) ); } return true; } /** * Creates a single user. * * @since 4.7.0 * * @param WP_REST_Request $request Full details about the request. * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. */ public function create_item( $request ) { if ( ! empty( $request['id'] ) ) { return new WP_Error( 'rest_user_exists', __( 'Cannot create existing user.' ), array( 'status' => 400 ) ); } $schema = $this->get_item_schema(); if ( ! empty( $request['roles'] ) && ! empty( $schema['properties']['roles'] ) ) { $check_permission = $this->check_role_update( $request['id'], $request['roles'] ); if ( is_wp_error( $check_permission ) ) { return $check_permission; } } $user = $this->prepare_item_for_database( $request ); if ( is_multisite() ) { $ret = wpmu_validate_user_signup( $user->user_login, $user->user_email ); if ( is_wp_error( $ret['errors'] ) && $ret['errors']->has_errors() ) { $error = new WP_Error( 'rest_invalid_param', __( 'Invalid user parameter(s).' ), array( 'status' => 400 ) ); foreach ( $ret['errors']->errors as $code => $messages ) { foreach ( $messages as $message ) { $error->add( $code, $message ); } $error_data = $error->get_error_data( $code ); if ( $error_data ) { $error->add_data( $error_data, $code ); } } return $error; } } if ( is_multisite() ) { $user_id = wpmu_create_user( $user->user_login, $user->user_pass, $user->user_email ); if ( ! $user_id ) { return new WP_Error( 'rest_user_create', __( 'Error creating new user.' ), array( 'status' => 500 ) ); } $user->ID = $user_id; $user_id = wp_update_user( wp_slash( (array) $user ) ); if ( is_wp_error( $user_id ) ) { return $user_id; } $result = add_user_to_blog( get_site()->id, $user_id, '' ); if ( is_wp_error( $result ) ) { return $result; } } else { $user_id = wp_insert_user( wp_slash( (array) $user ) ); if ( is_wp_error( $user_id ) ) { return $user_id; } } $user = get_user_by( 'id', $user_id ); /** * Fires immediately after a user is created or updated via the REST API. * * @since 4.7.0 * * @param WP_User $user Inserted or updated user object. * @param WP_REST_Request $request Request object. * @param bool $creating True when creating a user, false when updating. */ do_action( 'rest_insert_user', $user, $request, true ); if ( ! empty( $request['roles'] ) && ! empty( $schema['properties']['roles'] ) ) { array_map( array( $user, 'add_role' ), $request['roles'] ); } if ( ! empty( $schema['properties']['meta'] ) && isset( $request['meta'] ) ) { $meta_update = $this->meta->update_value( $request['meta'], $user_id ); if ( is_wp_error( $meta_update ) ) { return $meta_update; } } $user = get_user_by( 'id', $user_id ); $fields_update = $this->update_additional_fields_for_object( $user, $request ); if ( is_wp_error( $fields_update ) ) { return $fields_update; } $request->set_param( 'context', 'edit' ); /** * Fires after a user is completely created or updated via the REST API. * * @since 5.0.0 * * @param WP_User $user Inserted or updated user object. * @param WP_REST_Request $request Request object. * @param bool $creating True when creating a user, false when updating. */ do_action( 'rest_after_insert_user', $user, $request, true ); $response = $this->prepare_item_for_response( $user, $request ); $response = rest_ensure_response( $response ); $response->set_status( 201 ); $response->header( 'Location', rest_url( sprintf( '%s/%s/%d', $this->namespace, $this->rest_base, $user_id ) ) ); return $response; } /** * Checks if a given request has access to update a user. * * @since 4.7.0 * * @param WP_REST_Request $request Full details about the request. * @return true|WP_Error True if the request has access to update the item, WP_Error object otherwise. */ public function update_item_permissions_check( $request ) { $user = $this->get_user( $request['id'] ); if ( is_wp_error( $user ) ) { return $user; } if ( ! empty( $request['roles'] ) ) { if ( ! current_user_can( 'promote_user', $user->ID ) ) { return new WP_Error( 'rest_cannot_edit_roles', __( 'Sorry, you are not allowed to edit roles of this user.' ), array( 'status' => rest_authorization_required_code() ) ); } $request_params = array_keys( $request->get_params() ); sort( $request_params ); // If only 'id' and 'roles' are specified (we are only trying to // edit roles), then only the 'promote_user' cap is required. if ( array( 'id', 'roles' ) === $request_params ) { return true; } } if ( ! current_user_can( 'edit_user', $user->ID ) ) { return new WP_Error( 'rest_cannot_edit', __( 'Sorry, you are not allowed to edit this user.' ), array( 'status' => rest_authorization_required_code() ) ); } return true; } /** * Updates a single user. * * @since 4.7.0 * * @param WP_REST_Request $request Full details about the request. * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. */ public function update_item( $request ) { $user = $this->get_user( $request['id'] ); if ( is_wp_error( $user ) ) { return $user; } $id = $user->ID; $owner_id = false; if ( is_string( $request['email'] ) ) { $owner_id = email_exists( $request['email'] ); } if ( $owner_id && $owner_id !== $id ) { return new WP_Error( 'rest_user_invalid_email', __( 'Invalid email address.' ), array( 'status' => 400 ) ); } if ( ! empty( $request['username'] ) && $request['username'] !== $user->user_login ) { return new WP_Error( 'rest_user_invalid_argument', __( 'Username is not editable.' ), array( 'status' => 400 ) ); } if ( ! empty( $request['slug'] ) && $request['slug'] !== $user->user_nicename && get_user_by( 'slug', $request['slug'] ) ) { return new WP_Error( 'rest_user_invalid_slug', __( 'Invalid slug.' ), array( 'status' => 400 ) ); } if ( ! empty( $request['roles'] ) ) { $check_permission = $this->check_role_update( $id, $request['roles'] ); if ( is_wp_error( $check_permission ) ) { return $check_permission; } } $user = $this->prepare_item_for_database( $request ); // Ensure we're operating on the same user we already checked. $user->ID = $id; $user_id = wp_update_user( wp_slash( (array) $user ) ); if ( is_wp_error( $user_id ) ) { return $user_id; } $user = get_user_by( 'id', $user_id ); /** This action is documented in wp-includes/rest-api/endpoints/class-wp-rest-users-controller.php */ do_action( 'rest_insert_user', $user, $request, false ); if ( ! empty( $request['roles'] ) ) { array_map( array( $user, 'add_role' ), $request['roles'] ); } $schema = $this->get_item_schema(); if ( ! empty( $schema['properties']['meta'] ) && isset( $request['meta'] ) ) { $meta_update = $this->meta->update_value( $request['meta'], $id ); if ( is_wp_error( $meta_update ) ) { return $meta_update; } } $user = get_user_by( 'id', $user_id ); $fields_update = $this->update_additional_fields_for_object( $user, $request ); if ( is_wp_error( $fields_update ) ) { return $fields_update; } $request->set_param( 'context', 'edit' ); /** This action is documented in wp-includes/rest-api/endpoints/class-wp-rest-users-controller.php */ do_action( 'rest_after_insert_user', $user, $request, false ); $response = $this->prepare_item_for_response( $user, $request ); $response = rest_ensure_response( $response ); return $response; } /** * Checks if a given request has access to update the current user. * * @since 4.7.0 * * @param WP_REST_Request $request Full details about the request. * @return true|WP_Error True if the request has access to update the item, WP_Error object otherwise. */ public function update_current_item_permissions_check( $request ) { $request['id'] = get_current_user_id(); return $this->update_item_permissions_check( $request ); } /** * Updates the current user. * * @since 4.7.0 * * @param WP_REST_Request $request Full details about the request. * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. */ public function update_current_item( $request ) { $request['id'] = get_current_user_id(); return $this->update_item( $request ); } /** * Checks if a given request has access delete a user. * * @since 4.7.0 * * @param WP_REST_Request $request Full details about the request. * @return true|WP_Error True if the request has access to delete the item, WP_Error object otherwise. */ public function delete_item_permissions_check( $request ) { $user = $this->get_user( $request['id'] ); if ( is_wp_error( $user ) ) { return $user; } if ( ! current_user_can( 'delete_user', $user->ID ) ) { return new WP_Error( 'rest_user_cannot_delete', __( 'Sorry, you are not allowed to delete this user.' ), array( 'status' => rest_authorization_required_code() ) ); } return true; } /** * Deletes a single user. * * @since 4.7.0 * * @param WP_REST_Request $request Full details about the request. * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. */ public function delete_item( $request ) { // We don't support delete requests in multisite. if ( is_multisite() ) { return new WP_Error( 'rest_cannot_delete', __( 'The user cannot be deleted.' ), array( 'status' => 501 ) ); } $user = $this->get_user( $request['id'] ); if ( is_wp_error( $user ) ) { return $user; } $id = $user->ID; $reassign = false === $request['reassign'] ? null : absint( $request['reassign'] ); $force = isset( $request['force'] ) ? (bool) $request['force'] : false; // We don't support trashing for users. if ( ! $force ) { return new WP_Error( 'rest_trash_not_supported', /* translators: %s: force=true */ sprintf( __( "Users do not support trashing. Set '%s' to delete." ), 'force=true' ), array( 'status' => 501 ) ); } if ( ! empty( $reassign ) ) { if ( $reassign === $id || ! get_userdata( $reassign ) ) { return new WP_Error( 'rest_user_invalid_reassign', __( 'Invalid user ID for reassignment.' ), array( 'status' => 400 ) ); } } $request->set_param( 'context', 'edit' ); $previous = $this->prepare_item_for_response( $user, $request ); // Include user admin functions to get access to wp_delete_user(). require_once ABSPATH . 'wp-admin/includes/user.php'; $result = wp_delete_user( $id, $reassign ); if ( ! $result ) { return new WP_Error( 'rest_cannot_delete', __( 'The user cannot be deleted.' ), array( 'status' => 500 ) ); } $response = new WP_REST_Response(); $response->set_data( array( 'deleted' => true, 'previous' => $previous->get_data(), ) ); /** * Fires immediately after a user is deleted via the REST API. * * @since 4.7.0 * * @param WP_User $user The user data. * @param WP_REST_Response $response The response returned from the API. * @param WP_REST_Request $request The request sent to the API. */ do_action( 'rest_delete_user', $user, $response, $request ); return $response; } /** * Checks if a given request has access to delete the current user. * * @since 4.7.0 * * @param WP_REST_Request $request Full details about the request. * @return true|WP_Error True if the request has access to delete the item, WP_Error object otherwise. */ public function delete_current_item_permissions_check( $request ) { $request['id'] = get_current_user_id(); return $this->delete_item_permissions_check( $request ); } /** * Deletes the current user. * * @since 4.7.0 * * @param WP_REST_Request $request Full details about the request. * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. */ public function delete_current_item( $request ) { $request['id'] = get_current_user_id(); return $this->delete_item( $request ); } /** * Prepares a single user output for response. * * @since 4.7.0 * @since 5.9.0 Renamed `$user` to `$item` to match parent class for PHP 8 named parameter support. * * @param WP_User $item User object. * @param WP_REST_Request $request Request object. * @return WP_REST_Response Response object. */ public function prepare_item_for_response( $item, $request ) { // Restores the more descriptive, specific name for use within this method. $user = $item; $data = array(); $fields = $this->get_fields_for_response( $request ); if ( in_array( 'id', $fields, true ) ) { $data['id'] = $user->ID; } if ( in_array( 'username', $fields, true ) ) { $data['username'] = $user->user_login; } if ( in_array( 'name', $fields, true ) ) { $data['name'] = $user->display_name; } if ( in_array( 'first_name', $fields, true ) ) { $data['first_name'] = $user->first_name; } if ( in_array( 'last_name', $fields, true ) ) { $data['last_name'] = $user->last_name; } if ( in_array( 'email', $fields, true ) ) { $data['email'] = $user->user_email; } if ( in_array( 'url', $fields, true ) ) { $data['url'] = $user->user_url; } if ( in_array( 'description', $fields, true ) ) { $data['description'] = $user->description; } if ( in_array( 'link', $fields, true ) ) { $data['link'] = get_author_posts_url( $user->ID, $user->user_nicename ); } if ( in_array( 'locale', $fields, true ) ) { $data['locale'] = get_user_locale( $user ); } if ( in_array( 'nickname', $fields, true ) ) { $data['nickname'] = $user->nickname; } if ( in_array( 'slug', $fields, true ) ) { $data['slug'] = $user->user_nicename; } if ( in_array( 'roles', $fields, true ) ) { // Defensively call array_values() to ensure an array is returned. $data['roles'] = array_values( $user->roles ); } if ( in_array( 'registered_date', $fields, true ) ) { $data['registered_date'] = gmdate( 'c', strtotime( $user->user_registered ) ); } if ( in_array( 'capabilities', $fields, true ) ) { $data['capabilities'] = (object) $user->allcaps; } if ( in_array( 'extra_capabilities', $fields, true ) ) { $data['extra_capabilities'] = (object) $user->caps; } if ( in_array( 'avatar_urls', $fields, true ) ) { $data['avatar_urls'] = rest_get_avatar_urls( $user ); } if ( in_array( 'meta', $fields, true ) ) { $data['meta'] = $this->meta->get_value( $user->ID, $request ); } $context = ! empty( $request['context'] ) ? $request['context'] : 'embed'; $data = $this->add_additional_fields_to_object( $data, $request ); $data = $this->filter_response_by_context( $data, $context ); // Wrap the data in a response object. $response = rest_ensure_response( $data ); if ( rest_is_field_included( '_links', $fields ) || rest_is_field_included( '_embedded', $fields ) ) { $response->add_links( $this->prepare_links( $user ) ); } /** * Filters user data returned from the REST API. * * @since 4.7.0 * * @param WP_REST_Response $response The response object. * @param WP_User $user User object used to create response. * @param WP_REST_Request $request Request object. */ return apply_filters( 'rest_prepare_user', $response, $user, $request ); } /** * Prepares links for the user request. * * @since 4.7.0 * * @param WP_User $user User object. * @return array Links for the given user. */ protected function prepare_links( $user ) { $links = array( 'self' => array( 'href' => rest_url( sprintf( '%s/%s/%d', $this->namespace, $this->rest_base, $user->ID ) ), ), 'collection' => array( 'href' => rest_url( sprintf( '%s/%s', $this->namespace, $this->rest_base ) ), ), ); return $links; } /** * Prepares a single user for creation or update. * * @since 4.7.0 * * @param WP_REST_Request $request Request object. * @return object User object. */ protected function prepare_item_for_database( $request ) { $prepared_user = new stdClass(); $schema = $this->get_item_schema(); // Required arguments. if ( isset( $request['email'] ) && ! empty( $schema['properties']['email'] ) ) { $prepared_user->user_email = $request['email']; } if ( isset( $request['username'] ) && ! empty( $schema['properties']['username'] ) ) { $prepared_user->user_login = $request['username']; } if ( isset( $request['password'] ) && ! empty( $schema['properties']['password'] ) ) { $prepared_user->user_pass = $request['password']; } // Optional arguments. if ( isset( $request['id'] ) ) { $prepared_user->ID = absint( $request['id'] ); } if ( isset( $request['name'] ) && ! empty( $schema['properties']['name'] ) ) { $prepared_user->display_name = $request['name']; } if ( isset( $request['first_name'] ) && ! empty( $schema['properties']['first_name'] ) ) { $prepared_user->first_name = $request['first_name']; } if ( isset( $request['last_name'] ) && ! empty( $schema['properties']['last_name'] ) ) { $prepared_user->last_name = $request['last_name']; } if ( isset( $request['nickname'] ) && ! empty( $schema['properties']['nickname'] ) ) { $prepared_user->nickname = $request['nickname']; } if ( isset( $request['slug'] ) && ! empty( $schema['properties']['slug'] ) ) { $prepared_user->user_nicename = $request['slug']; } if ( isset( $request['description'] ) && ! empty( $schema['properties']['description'] ) ) { $prepared_user->description = $request['description']; } if ( isset( $request['url'] ) && ! empty( $schema['properties']['url'] ) ) { $prepared_user->user_url = $request['url']; } if ( isset( $request['locale'] ) && ! empty( $schema['properties']['locale'] ) ) { $prepared_user->locale = $request['locale']; } // Setting roles will be handled outside of this function. if ( isset( $request['roles'] ) ) { $prepared_user->role = false; } /** * Filters user data before insertion via the REST API. * * @since 4.7.0 * * @param object $prepared_user User object. * @param WP_REST_Request $request Request object. */ return apply_filters( 'rest_pre_insert_user', $prepared_user, $request ); } /** * Determines if the current user is allowed to make the desired roles change. * * @since 4.7.0 * * @global WP_Roles $wp_roles WordPress role management object. * * @param int $user_id User ID. * @param array $roles New user roles. * @return true|WP_Error True if the current user is allowed to make the role change, * otherwise a WP_Error object. */ protected function check_role_update( $user_id, $roles ) { global $wp_roles; foreach ( $roles as $role ) { if ( ! isset( $wp_roles->role_objects[ $role ] ) ) { return new WP_Error( 'rest_user_invalid_role', /* translators: %s: Role key. */ sprintf( __( 'The role %s does not exist.' ), $role ), array( 'status' => 400 ) ); } $potential_role = $wp_roles->role_objects[ $role ]; /* * Don't let anyone with 'edit_users' (admins) edit their own role to something without it. * Multisite super admins can freely edit their blog roles -- they possess all caps. */ if ( ! ( is_multisite() && current_user_can( 'manage_sites' ) ) && get_current_user_id() === $user_id && ! $potential_role->has_cap( 'edit_users' ) ) { return new WP_Error( 'rest_user_invalid_role', __( 'Sorry, you are not allowed to give users that role.' ), array( 'status' => rest_authorization_required_code() ) ); } // Include user admin functions to get access to get_editable_roles(). require_once ABSPATH . 'wp-admin/includes/user.php'; // The new role must be editable by the logged-in user. $editable_roles = get_editable_roles(); if ( empty( $editable_roles[ $role ] ) ) { return new WP_Error( 'rest_user_invalid_role', __( 'Sorry, you are not allowed to give users that role.' ), array( 'status' => 403 ) ); } } return true; } /** * Check a username for the REST API. * * Performs a couple of checks like edit_user() in wp-admin/includes/user.php. * * @since 4.7.0 * * @param string $value The username submitted in the request. * @param WP_REST_Request $request Full details about the request. * @param string $param The parameter name. * @return string|WP_Error The sanitized username, if valid, otherwise an error. */ public function check_username( $value, $request, $param ) { $username = (string) $value; if ( ! validate_username( $username ) ) { return new WP_Error( 'rest_user_invalid_username', __( 'This username is invalid because it uses illegal characters. Please enter a valid username.' ), array( 'status' => 400 ) ); } /** This filter is documented in wp-includes/user.php */ $illegal_logins = (array) apply_filters( 'illegal_user_logins', array() ); if ( in_array( strtolower( $username ), array_map( 'strtolower', $illegal_logins ), true ) ) { return new WP_Error( 'rest_user_invalid_username', __( 'Sorry, that username is not allowed.' ), array( 'status' => 400 ) ); } return $username; } /** * Check a user password for the REST API. * * Performs a couple of checks like edit_user() in wp-admin/includes/user.php. * * @since 4.7.0 * * @param string $value The password submitted in the request. * @param WP_REST_Request $request Full details about the request. * @param string $param The parameter name. * @return string|WP_Error The sanitized password, if valid, otherwise an error. */ public function check_user_password( $value, $request, $param ) { $password = (string) $value; if ( empty( $password ) ) { return new WP_Error( 'rest_user_invalid_password', __( 'Passwords cannot be empty.' ), array( 'status' => 400 ) ); } if ( false !== strpos( $password, '\\' ) ) { return new WP_Error( 'rest_user_invalid_password', sprintf( /* translators: %s: The '\' character. */ __( 'Passwords cannot contain the "%s" character.' ), '\\' ), array( 'status' => 400 ) ); } return $password; } /** * Retrieves the user's schema, conforming to JSON Schema. * * @since 4.7.0 * * @return array Item schema data. */ public function get_item_schema() { if ( $this->schema ) { return $this->add_additional_fields_schema( $this->schema ); } $schema = array( '$schema' => 'http://json-schema.org/draft-04/schema#', 'title' => 'user', 'type' => 'object', 'properties' => array( 'id' => array( 'description' => __( 'Unique identifier for the user.' ), 'type' => 'integer', 'context' => array( 'embed', 'view', 'edit' ), 'readonly' => true, ), 'username' => array( 'description' => __( 'Login name for the user.' ), 'type' => 'string', 'context' => array( 'edit' ), 'required' => true, 'arg_options' => array( 'sanitize_callback' => array( $this, 'check_username' ), ), ), 'name' => array( 'description' => __( 'Display name for the user.' ), 'type' => 'string', 'context' => array( 'embed', 'view', 'edit' ), 'arg_options' => array( 'sanitize_callback' => 'sanitize_text_field', ), ), 'first_name' => array( 'description' => __( 'First name for the user.' ), 'type' => 'string', 'context' => array( 'edit' ), 'arg_options' => array( 'sanitize_callback' => 'sanitize_text_field', ), ), 'last_name' => array( 'description' => __( 'Last name for the user.' ), 'type' => 'string', 'context' => array( 'edit' ), 'arg_options' => array( 'sanitize_callback' => 'sanitize_text_field', ), ), 'email' => array( 'description' => __( 'The email address for the user.' ), 'type' => 'string', 'format' => 'email', 'context' => array( 'edit' ), 'required' => true, ), 'url' => array( 'description' => __( 'URL of the user.' ), 'type' => 'string', 'format' => 'uri', 'context' => array( 'embed', 'view', 'edit' ), ), 'description' => array( 'description' => __( 'Description of the user.' ), 'type' => 'string', 'context' => array( 'embed', 'view', 'edit' ), ), 'link' => array( 'description' => __( 'Author URL of the user.' ), 'type' => 'string', 'format' => 'uri', 'context' => array( 'embed', 'view', 'edit' ), 'readonly' => true, ), 'locale' => array( 'description' => __( 'Locale for the user.' ), 'type' => 'string', 'enum' => array_merge( array( '', 'en_US' ), get_available_languages() ), 'context' => array( 'edit' ), ), 'nickname' => array( 'description' => __( 'The nickname for the user.' ), 'type' => 'string', 'context' => array( 'edit' ), 'arg_options' => array( 'sanitize_callback' => 'sanitize_text_field', ), ), 'slug' => array( 'description' => __( 'An alphanumeric identifier for the user.' ), 'type' => 'string', 'context' => array( 'embed', 'view', 'edit' ), 'arg_options' => array( 'sanitize_callback' => array( $this, 'sanitize_slug' ), ), ), 'registered_date' => array( 'description' => __( 'Registration date for the user.' ), 'type' => 'string', 'format' => 'date-time', 'context' => array( 'edit' ), 'readonly' => true, ), 'roles' => array( 'description' => __( 'Roles assigned to the user.' ), 'type' => 'array', 'items' => array( 'type' => 'string', ), 'context' => array( 'edit' ), ), 'password' => array( 'description' => __( 'Password for the user (never included).' ), 'type' => 'string', 'context' => array(), // Password is never displayed. 'required' => true, 'arg_options' => array( 'sanitize_callback' => array( $this, 'check_user_password' ), ), ), 'capabilities' => array( 'description' => __( 'All capabilities assigned to the user.' ), 'type' => 'object', 'context' => array( 'edit' ), 'readonly' => true, ), 'extra_capabilities' => array( 'description' => __( 'Any extra capabilities assigned to the user.' ), 'type' => 'object', 'context' => array( 'edit' ), 'readonly' => true, ), ), ); if ( get_option( 'show_avatars' ) ) { $avatar_properties = array(); $avatar_sizes = rest_get_avatar_sizes(); foreach ( $avatar_sizes as $size ) { $avatar_properties[ $size ] = array( /* translators: %d: Avatar image size in pixels. */ 'description' => sprintf( __( 'Avatar URL with image size of %d pixels.' ), $size ), 'type' => 'string', 'format' => 'uri', 'context' => array( 'embed', 'view', 'edit' ), ); } $schema['properties']['avatar_urls'] = array( 'description' => __( 'Avatar URLs for the user.' ), 'type' => 'object', 'context' => array( 'embed', 'view', 'edit' ), 'readonly' => true, 'properties' => $avatar_properties, ); } $schema['properties']['meta'] = $this->meta->get_field_schema(); $this->schema = $schema; return $this->add_additional_fields_schema( $this->schema ); } /** * Retrieves the query params for collections. * * @since 4.7.0 * * @return array Collection parameters. */ public function get_collection_params() { $query_params = parent::get_collection_params(); $query_params['context']['default'] = 'view'; $query_params['exclude'] = array( 'description' => __( 'Ensure result set excludes specific IDs.' ), 'type' => 'array', 'items' => array( 'type' => 'integer', ), 'default' => array(), ); $query_params['include'] = array( 'description' => __( 'Limit result set to specific IDs.' ), 'type' => 'array', 'items' => array( 'type' => 'integer', ), 'default' => array(), ); $query_params['offset'] = array( 'description' => __( 'Offset the result set by a specific number of items.' ), 'type' => 'integer', ); $query_params['order'] = array( 'default' => 'asc', 'description' => __( 'Order sort attribute ascending or descending.' ), 'enum' => array( 'asc', 'desc' ), 'type' => 'string', ); $query_params['orderby'] = array( 'default' => 'name', 'description' => __( 'Sort collection by user attribute.' ), 'enum' => array( 'id', 'include', 'name', 'registered_date', 'slug', 'include_slugs', 'email', 'url', ), 'type' => 'string', ); $query_params['slug'] = array( 'description' => __( 'Limit result set to users with one or more specific slugs.' ), 'type' => 'array', 'items' => array( 'type' => 'string', ), ); $query_params['roles'] = array( 'description' => __( 'Limit result set to users matching at least one specific role provided. Accepts csv list or single role.' ), 'type' => 'array', 'items' => array( 'type' => 'string', ), ); $query_params['capabilities'] = array( 'description' => __( 'Limit result set to users matching at least one specific capability provided. Accepts csv list or single capability.' ), 'type' => 'array', 'items' => array( 'type' => 'string', ), ); $query_params['who'] = array( 'description' => __( 'Limit result set to users who are considered authors.' ), 'type' => 'string', 'enum' => array( 'authors', ), ); $query_params['has_published_posts'] = array( 'description' => __( 'Limit result set to users who have published posts.' ), 'type' => array( 'boolean', 'array' ), 'items' => array( 'type' => 'string', 'enum' => get_post_types( array( 'show_in_rest' => true ), 'names' ), ), ); /** * Filters REST API collection parameters for the users controller. * * This filter registers the collection parameter, but does not map the * collection parameter to an internal WP_User_Query parameter. Use the * `rest_user_query` filter to set WP_User_Query arguments. * * @since 4.7.0 * * @param array $query_params JSON Schema-formatted collection parameters. */ return apply_filters( 'rest_user_collection_params', $query_params ); } }