#bPNG IHDR Ÿ f Õ†C1 sRGB ®Îé gAMA ±üa pHYs à ÃÇo¨d GIDATx^íÜL”÷ð÷Yçªö("Bh_ò«®¸¢§q5kÖ*:þ0AºšÖ¥]VkJ¢M»¶f¸±8\k2íll£1]q®ÙÔ‚ÆT p0 name = 'feed_' . $name; $this->mod_name = 'feed_mod_' . $name; $lifetime = $this->lifetime; /** * Filters the transient lifetime of the feed cache. * * @since 2.8.0 * * @param int $lifetime Cache duration in seconds. Default is 43200 seconds (12 hours). * @param string $name Unique identifier for the cache object. */ $this->lifetime = apply_filters( 'wp_feed_cache_transient_lifetime', $lifetime, $name ); } /** * Saves data to the transient. * * @since 2.8.0 * * @param array|SimplePie\SimplePie $data Data to save. If passed a SimplePie object, * only cache the `$data` property. * @return true Always true. */ public function save( $data ) { if ( $data instanceof SimplePie\SimplePie ) { $data = $data->data; } set_site_transient( $this->name, $data, $this->lifetime ); set_site_transient( $this->mod_name, time(), $this->lifetime ); return true; } /** * Retrieves the data saved in the transient. * * @since 2.8.0 * * @return array Data for `SimplePie::$data`. */ public function load() { return get_site_transient( $this->name ); } /** * Gets mod transient. * * @since 2.8.0 * * @return int Timestamp. */ public function mtime() { return get_site_transient( $this->mod_name ); } /** * Sets mod transient. * * @since 2.8.0 * * @return bool False if value was not set and true if value was set. */ public function touch() { return set_site_transient( $this->mod_name, time(), $this->lifetime ); } /** * Deletes transients. * * @since 2.8.0 * * @return true Always true. */ public function unlink() { delete_site_transient( $this->name ); delete_site_transient( $this->mod_name ); return true; } } 'border.radius', 'spacing.customMargin' => 'spacing.margin', 'spacing.customPadding' => 'spacing.padding', 'typography.customLineHeight' => 'typography.lineHeight', ); /** * Function that migrates a given theme.json structure to the last version. * * @since 5.9.0 * @since 6.6.0 Migrate up to v3 and add $origin parameter. * * @param array $theme_json The structure to migrate. * @param string $origin Optional. What source of data this object represents. * One of 'blocks', 'default', 'theme', or 'custom'. Default 'theme'. * @return array The structure in the last version. */ public static function migrate( $theme_json, $origin = 'theme' ) { if ( ! isset( $theme_json['version'] ) ) { $theme_json = array( 'version' => WP_Theme_JSON::LATEST_SCHEMA, ); } // Migrate each version in order starting with the current version. switch ( $theme_json['version'] ) { case 1: $theme_json = self::migrate_v1_to_v2( $theme_json ); // Deliberate fall through. Once migrated to v2, also migrate to v3. case 2: $theme_json = self::migrate_v2_to_v3( $theme_json, $origin ); } return $theme_json; } /** * Removes the custom prefixes for a few properties * that were part of v1: * * 'border.customRadius' => 'border.radius', * 'spacing.customMargin' => 'spacing.margin', * 'spacing.customPadding' => 'spacing.padding', * 'typography.customLineHeight' => 'typography.lineHeight', * * @since 5.9.0 * * @param array $old Data to migrate. * * @return array Data without the custom prefixes. */ private static function migrate_v1_to_v2( $old ) { // Copy everything. $new = $old; // Overwrite the things that changed. if ( isset( $old['settings'] ) ) { $new['settings'] = self::rename_paths( $old['settings'], self::V1_TO_V2_RENAMED_PATHS ); } // Set the new version. $new['version'] = 2; return $new; } /** * Migrates from v2 to v3. * * - Sets settings.typography.defaultFontSizes to false if settings.typography.fontSizes are defined. * - Sets settings.spacing.defaultSpacingSizes to false if settings.spacing.spacingSizes are defined. * - Prevents settings.spacing.spacingSizes from merging with settings.spacing.spacingScale by * unsetting spacingScale when spacingSizes are defined. * * @since 6.6.0 * * @param array $old Data to migrate. * @param string $origin What source of data this object represents. * One of 'blocks', 'default', 'theme', or 'custom'. * @return array Data with defaultFontSizes set to false. */ private static function migrate_v2_to_v3( $old, $origin ) { // Copy everything. $new = $old; // Set the new version. $new['version'] = 3; /* * Remaining changes do not need to be applied to the custom origin, * as they should take on the value of the theme origin. */ if ( 'custom' === $origin ) { return $new; } /* * Even though defaultFontSizes and defaultSpacingSizes are new * settings, we need to migrate them as they each control * PRESETS_METADATA prevent_override values which were previously * hardcoded to false. This only needs to happen when the theme provides * fontSizes or spacingSizes as they could match the default ones and * affect the generated CSS. */ if ( isset( $old['settings']['typography']['fontSizes'] ) ) { $new['settings']['typography']['defaultFontSizes'] = false; } /* * Similarly to defaultFontSizes, we need to migrate defaultSpacingSizes * as it controls the PRESETS_METADATA prevent_override which was * previously hardcoded to false. This only needs to happen when the * theme provided spacing sizes via spacingSizes or spacingScale. */ if ( isset( $old['settings']['spacing']['spacingSizes'] ) || isset( $old['settings']['spacing']['spacingScale'] ) ) { $new['settings']['spacing']['defaultSpacingSizes'] = false; } /* * In v3 spacingSizes is merged with the generated spacingScale sizes * instead of completely replacing them. The v3 behavior is what was * documented for the v2 schema, but the code never actually did work * that way. Instead of surprising users with a behavior change two * years after the fact at the same time as a v3 update is introduced, * we'll continue using the "bugged" behavior for v2 themes. And treat * the "bug fix" as a breaking change for v3. */ if ( isset( $old['settings']['spacing']['spacingSizes'] ) ) { unset( $new['settings']['spacing']['spacingScale'] ); } return $new; } /** * Processes the settings subtree. * * @since 5.9.0 * * @param array $settings Array to process. * @param array $paths_to_rename Paths to rename. * * @return array The settings in the new format. */ private static function rename_paths( $settings, $paths_to_rename ) { $new_settings = $settings; // Process any renamed/moved paths within default settings. self::rename_settings( $new_settings, $paths_to_rename ); // Process individual block settings. if ( isset( $new_settings['blocks'] ) && is_array( $new_settings['blocks'] ) ) { foreach ( $new_settings['blocks'] as &$block_settings ) { self::rename_settings( $block_settings, $paths_to_rename ); } } return $new_settings; } /** * Processes a settings array, renaming or moving properties. * * @since 5.9.0 * * @param array $settings Reference to settings either defaults or an individual block's. * @param array $paths_to_rename Paths to rename. */ private static function rename_settings( &$settings, $paths_to_rename ) { foreach ( $paths_to_rename as $original => $renamed ) { $original_path = explode( '.', $original ); $renamed_path = explode( '.', $renamed ); $current_value = _wp_array_get( $settings, $original_path, null ); if ( null !== $current_value ) { _wp_array_set( $settings, $renamed_path, $current_value ); self::unset_setting_by_path( $settings, $original_path ); } } } /** * Removes a property from within the provided settings by its path. * * @since 5.9.0 * * @param array $settings Reference to the current settings array. * @param array $path Path to the property to be removed. */ private static function unset_setting_by_path( &$settings, $path ) { $tmp_settings = &$settings; $last_key = array_pop( $path ); foreach ( $path as $key ) { $tmp_settings = &$tmp_settings[ $key ]; } unset( $tmp_settings[ $last_key ] ); } } methods = array( // WordPress API. 'wp.getUsersBlogs' => 'this:wp_getUsersBlogs', 'wp.newPost' => 'this:wp_newPost', 'wp.editPost' => 'this:wp_editPost', 'wp.deletePost' => 'this:wp_deletePost', 'wp.getPost' => 'this:wp_getPost', 'wp.getPosts' => 'this:wp_getPosts', 'wp.newTerm' => 'this:wp_newTerm', 'wp.editTerm' => 'this:wp_editTerm', 'wp.deleteTerm' => 'this:wp_deleteTerm', 'wp.getTerm' => 'this:wp_getTerm', 'wp.getTerms' => 'this:wp_getTerms', 'wp.getTaxonomy' => 'this:wp_getTaxonomy', 'wp.getTaxonomies' => 'this:wp_getTaxonomies', 'wp.getUser' => 'this:wp_getUser', 'wp.getUsers' => 'this:wp_getUsers', 'wp.getProfile' => 'this:wp_getProfile', 'wp.editProfile' => 'this:wp_editProfile', 'wp.getPage' => 'this:wp_getPage', 'wp.getPages' => 'this:wp_getPages', 'wp.newPage' => 'this:wp_newPage', 'wp.deletePage' => 'this:wp_deletePage', 'wp.editPage' => 'this:wp_editPage', 'wp.getPageList' => 'this:wp_getPageList', 'wp.getAuthors' => 'this:wp_getAuthors', 'wp.getCategories' => 'this:mw_getCategories', // Alias. 'wp.getTags' => 'this:wp_getTags', 'wp.newCategory' => 'this:wp_newCategory', 'wp.deleteCategory' => 'this:wp_deleteCategory', 'wp.suggestCategories' => 'this:wp_suggestCategories', 'wp.uploadFile' => 'this:mw_newMediaObject', // Alias. 'wp.deleteFile' => 'this:wp_deletePost', // Alias. 'wp.getCommentCount' => 'this:wp_getCommentCount', 'wp.getPostStatusList' => 'this:wp_getPostStatusList', 'wp.getPageStatusList' => 'this:wp_getPageStatusList', 'wp.getPageTemplates' => 'this:wp_getPageTemplates', 'wp.getOptions' => 'this:wp_getOptions', 'wp.setOptions' => 'this:wp_setOptions', 'wp.getComment' => 'this:wp_getComment', 'wp.getComments' => 'this:wp_getComments', 'wp.deleteComment' => 'this:wp_deleteComment', 'wp.editComment' => 'this:wp_editComment', 'wp.newComment' => 'this:wp_newComment', 'wp.getCommentStatusList' => 'this:wp_getCommentStatusList', 'wp.getMediaItem' => 'this:wp_getMediaItem', 'wp.getMediaLibrary' => 'this:wp_getMediaLibrary', 'wp.getPostFormats' => 'this:wp_getPostFormats', 'wp.getPostType' => 'this:wp_getPostType', 'wp.getPostTypes' => 'this:wp_getPostTypes', 'wp.getRevisions' => 'this:wp_getRevisions', 'wp.restoreRevision' => 'this:wp_restoreRevision', // Blogger API. 'blogger.getUsersBlogs' => 'this:blogger_getUsersBlogs', 'blogger.getUserInfo' => 'this:blogger_getUserInfo', 'blogger.getPost' => 'this:blogger_getPost', 'blogger.getRecentPosts' => 'this:blogger_getRecentPosts', 'blogger.newPost' => 'this:blogger_newPost', 'blogger.editPost' => 'this:blogger_editPost', 'blogger.deletePost' => 'this:blogger_deletePost', // MetaWeblog API (with MT extensions to structs). 'metaWeblog.newPost' => 'this:mw_newPost', 'metaWeblog.editPost' => 'this:mw_editPost', 'metaWeblog.getPost' => 'this:mw_getPost', 'metaWeblog.getRecentPosts' => 'this:mw_getRecentPosts', 'metaWeblog.getCategories' => 'this:mw_getCategories', 'metaWeblog.newMediaObject' => 'this:mw_newMediaObject', /* * MetaWeblog API aliases for Blogger API. * See http://www.xmlrpc.com/stories/storyReader$2460 */ 'metaWeblog.deletePost' => 'this:blogger_deletePost', 'metaWeblog.getUsersBlogs' => 'this:blogger_getUsersBlogs', // MovableType API. 'mt.getCategoryList' => 'this:mt_getCategoryList', 'mt.getRecentPostTitles' => 'this:mt_getRecentPostTitles', 'mt.getPostCategories' => 'this:mt_getPostCategories', 'mt.setPostCategories' => 'this:mt_setPostCategories', 'mt.supportedMethods' => 'this:mt_supportedMethods', 'mt.supportedTextFilters' => 'this:mt_supportedTextFilters', 'mt.getTrackbackPings' => 'this:mt_getTrackbackPings', 'mt.publishPost' => 'this:mt_publishPost', // Pingback. 'pingback.ping' => 'this:pingback_ping', 'pingback.extensions.getPingbacks' => 'this:pingback_extensions_getPingbacks', 'demo.sayHello' => 'this:sayHello', 'demo.addTwoNumbers' => 'this:addTwoNumbers', ); $this->initialise_blog_option_info(); /** * Filters the methods exposed by the XML-RPC server. * * This filter can be used to add new methods, and remove built-in methods. * * @since 1.5.0 * * @param string[] $methods An array of XML-RPC methods, keyed by their methodName. */ $this->methods = apply_filters( 'xmlrpc_methods', $this->methods ); $this->set_is_enabled(); } /** * Sets wp_xmlrpc_server::$is_enabled property. * * Determines whether the xmlrpc server is enabled on this WordPress install * and set the is_enabled property accordingly. * * @since 5.7.3 */ private function set_is_enabled() { /* * Respect old get_option() filters left for back-compat when the 'enable_xmlrpc' * option was deprecated in 3.5.0. Use the {@see 'xmlrpc_enabled'} hook instead. */ $is_enabled = apply_filters( 'pre_option_enable_xmlrpc', false ); if ( false === $is_enabled ) { $is_enabled = apply_filters( 'option_enable_xmlrpc', true ); } /** * Filters whether XML-RPC methods requiring authentication are enabled. * * Contrary to the way it's named, this filter does not control whether XML-RPC is *fully* * enabled, rather, it only controls whether XML-RPC methods requiring authentication - * such as for publishing purposes - are enabled. * * Further, the filter does not control whether pingbacks or other custom endpoints that don't * require authentication are enabled. This behavior is expected, and due to how parity was matched * with the `enable_xmlrpc` UI option the filter replaced when it was introduced in 3.5. * * To disable XML-RPC methods that require authentication, use: * * add_filter( 'xmlrpc_enabled', '__return_false' ); * * For more granular control over all XML-RPC methods and requests, see the {@see 'xmlrpc_methods'} * and {@see 'xmlrpc_element_limit'} hooks. * * @since 3.5.0 * * @param bool $is_enabled Whether XML-RPC is enabled. Default true. */ $this->is_enabled = apply_filters( 'xmlrpc_enabled', $is_enabled ); } /** * Makes private/protected methods readable for backward compatibility. * * @since 4.0.0 * * @param string $name Method to call. * @param array $arguments Arguments to pass when calling. * @return array|IXR_Error|false Return value of the callback, false otherwise. */ public function __call( $name, $arguments ) { if ( '_multisite_getUsersBlogs' === $name ) { return $this->_multisite_getUsersBlogs( ...$arguments ); } return false; } /** * Serves the XML-RPC request. * * @since 2.9.0 */ public function serve_request() { $this->IXR_Server( $this->methods ); } /** * Tests XMLRPC API by saying, "Hello!" to client. * * @since 1.5.0 * * @return string Hello string response. */ public function sayHello() { return 'Hello!'; } /** * Tests XMLRPC API by adding two numbers for client. * * @since 1.5.0 * * @param array $args { * Method arguments. Note: arguments must be ordered as documented. * * @type int $0 A number to add. * @type int $1 A second number to add. * } * @return int Sum of the two given numbers. */ public function addTwoNumbers( $args ) { $number1 = $args[0]; $number2 = $args[1]; return $number1 + $number2; } /** * Logs user in. * * @since 2.8.0 * * @param string $username User's username. * @param string $password User's password. * @return WP_User|false WP_User object if authentication passed, false otherwise. */ public function login( $username, #[\SensitiveParameter] $password ) { if ( ! $this->is_enabled ) { $this->error = new IXR_Error( 405, sprintf( __( 'XML-RPC services are disabled on this site.' ) ) ); return false; } if ( $this->auth_failed ) { $user = new WP_Error( 'login_prevented' ); } else { $user = wp_authenticate( $username, $password ); } if ( is_wp_error( $user ) ) { $this->error = new IXR_Error( 403, __( 'Incorrect username or password.' ) ); // Flag that authentication has failed once on this wp_xmlrpc_server instance. $this->auth_failed = true; /** * Filters the XML-RPC user login error message. * * @since 3.5.0 * * @param IXR_Error $error The XML-RPC error message. * @param WP_Error $user WP_Error object. */ $this->error = apply_filters( 'xmlrpc_login_error', $this->error, $user ); return false; } wp_set_current_user( $user->ID ); return $user; } /** * Checks user's credentials. Deprecated. * * @since 1.5.0 * @deprecated 2.8.0 Use wp_xmlrpc_server::login() * @see wp_xmlrpc_server::login() * * @param string $username User's username. * @param string $password User's password. * @return bool Whether authentication passed. */ public function login_pass_ok( $username, #[\SensitiveParameter] $password ) { return (bool) $this->login( $username, $password ); } /** * Escapes string or array of strings for database. * * @since 1.5.2 * * @param string|array $data Escape single string or array of strings. * @return string|void Returns with string is passed, alters by-reference * when array is passed. */ public function escape( &$data ) { if ( ! is_array( $data ) ) { return wp_slash( $data ); } foreach ( $data as &$v ) { if ( is_array( $v ) ) { $this->escape( $v ); } elseif ( ! is_object( $v ) ) { $v = wp_slash( $v ); } } } /** * Sends error response to client. * * Sends an XML error response to the client. If the endpoint is enabled * an HTTP 200 response is always sent per the XML-RPC specification. * * @since 5.7.3 * * @param IXR_Error|string $error Error code or an error object. * @param false $message Error message. Optional. */ public function error( $error, $message = false ) { // Accepts either an error object or an error code and message if ( $message && ! is_object( $error ) ) { $error = new IXR_Error( $error, $message ); } if ( ! $this->is_enabled ) { status_header( $error->code ); } $this->output( $error->getXml() ); } /** * Retrieves custom fields for post. * * @since 2.5.0 * * @param int $post_id Post ID. * @return array Custom fields, if exist. */ public function get_custom_fields( $post_id ) { $post_id = (int) $post_id; $custom_fields = array(); foreach ( (array) has_meta( $post_id ) as $meta ) { // Don't expose protected fields. if ( ! current_user_can( 'edit_post_meta', $post_id, $meta['meta_key'] ) ) { continue; } $custom_fields[] = array( 'id' => $meta['meta_id'], 'key' => $meta['meta_key'], 'value' => $meta['meta_value'], ); } return $custom_fields; } /** * Sets custom fields for post. * * @since 2.5.0 * * @param int $post_id Post ID. * @param array $fields Custom fields. */ public function set_custom_fields( $post_id, $fields ) { $post_id = (int) $post_id; foreach ( (array) $fields as $meta ) { if ( isset( $meta['id'] ) ) { $meta['id'] = (int) $meta['id']; $pmeta = get_metadata_by_mid( 'post', $meta['id'] ); if ( ! $pmeta || (int) $pmeta->post_id !== $post_id ) { continue; } if ( isset( $meta['key'] ) ) { $meta['key'] = wp_unslash( $meta['key'] ); if ( $meta['key'] !== $pmeta->meta_key ) { continue; } $meta['value'] = wp_unslash( $meta['value'] ); if ( current_user_can( 'edit_post_meta', $post_id, $meta['key'] ) ) { update_metadata_by_mid( 'post', $meta['id'], $meta['value'] ); } } elseif ( current_user_can( 'delete_post_meta', $post_id, $pmeta->meta_key ) ) { delete_metadata_by_mid( 'post', $meta['id'] ); } } elseif ( current_user_can( 'add_post_meta', $post_id, wp_unslash( $meta['key'] ) ) ) { add_post_meta( $post_id, $meta['key'], $meta['value'] ); } } } /** * Retrieves custom fields for a term. * * @since 4.9.0 * * @param int $term_id Term ID. * @return array Array of custom fields, if they exist. */ public function get_term_custom_fields( $term_id ) { $term_id = (int) $term_id; $custom_fields = array(); foreach ( (array) has_term_meta( $term_id ) as $meta ) { if ( ! current_user_can( 'edit_term_meta', $term_id ) ) { continue; } $custom_fields[] = array( 'id' => $meta['meta_id'], 'key' => $meta['meta_key'], 'value' => $meta['meta_value'], ); } return $custom_fields; } /** * Sets custom fields for a term. * * @since 4.9.0 * * @param int $term_id Term ID. * @param array $fields Custom fields. */ public function set_term_custom_fields( $term_id, $fields ) { $term_id = (int) $term_id; foreach ( (array) $fields as $meta ) { if ( isset( $meta['id'] ) ) { $meta['id'] = (int) $meta['id']; $pmeta = get_metadata_by_mid( 'term', $meta['id'] ); if ( isset( $meta['key'] ) ) { $meta['key'] = wp_unslash( $meta['key'] ); if ( $meta['key'] !== $pmeta->meta_key ) { continue; } $meta['value'] = wp_unslash( $meta['value'] ); if ( current_user_can( 'edit_term_meta', $term_id ) ) { update_metadata_by_mid( 'term', $meta['id'], $meta['value'] ); } } elseif ( current_user_can( 'delete_term_meta', $term_id ) ) { delete_metadata_by_mid( 'term', $meta['id'] ); } } elseif ( current_user_can( 'add_term_meta', $term_id ) ) { add_term_meta( $term_id, $meta['key'], $meta['value'] ); } } } /** * Sets up blog options property. * * Passes property through {@see 'xmlrpc_blog_options'} filter. * * @since 2.6.0 */ public function initialise_blog_option_info() { $this->blog_options = array( // Read-only options. 'software_name' => array( 'desc' => __( 'Software Name' ), 'readonly' => true, 'value' => 'WordPress', ), 'software_version' => array( 'desc' => __( 'Software Version' ), 'readonly' => true, 'value' => get_bloginfo( 'version' ), ), 'blog_url' => array( 'desc' => __( 'WordPress Address (URL)' ), 'readonly' => true, 'option' => 'siteurl', ), 'home_url' => array( 'desc' => __( 'Site Address (URL)' ), 'readonly' => true, 'option' => 'home', ), 'login_url' => array( 'desc' => __( 'Login Address (URL)' ), 'readonly' => true, 'value' => wp_login_url(), ), 'admin_url' => array( 'desc' => __( 'The URL to the admin area' ), 'readonly' => true, 'value' => get_admin_url(), ), 'image_default_link_type' => array( 'desc' => __( 'Image default link type' ), 'readonly' => true, 'option' => 'image_default_link_type', ), 'image_default_size' => array( 'desc' => __( 'Image default size' ), 'readonly' => true, 'option' => 'image_default_size', ), 'image_default_align' => array( 'desc' => __( 'Image default align' ), 'readonly' => true, 'option' => 'image_default_align', ), 'template' => array( 'desc' => __( 'Template' ), 'readonly' => true, 'option' => 'template', ), 'stylesheet' => array( 'desc' => __( 'Stylesheet' ), 'readonly' => true, 'option' => 'stylesheet', ), 'post_thumbnail' => array( 'desc' => __( 'Post Thumbnail' ), 'readonly' => true, 'value' => current_theme_supports( 'post-thumbnails' ), ), // Updatable options. 'time_zone' => array( 'desc' => __( 'Time Zone' ), 'readonly' => false, 'option' => 'gmt_offset', ), 'blog_title' => array( 'desc' => __( 'Site Title' ), 'readonly' => false, 'option' => 'blogname', ), 'blog_tagline' => array( 'desc' => __( 'Site Tagline' ), 'readonly' => false, 'option' => 'blogdescription', ), 'date_format' => array( 'desc' => __( 'Date Format' ), 'readonly' => false, 'option' => 'date_format', ), 'time_format' => array( 'desc' => __( 'Time Format' ), 'readonly' => false, 'option' => 'time_format', ), 'users_can_register' => array( 'desc' => __( 'Allow new users to sign up' ), 'readonly' => false, 'option' => 'users_can_register', ), 'thumbnail_size_w' => array( 'desc' => __( 'Thumbnail Width' ), 'readonly' => false, 'option' => 'thumbnail_size_w', ), 'thumbnail_size_h' => array( 'desc' => __( 'Thumbnail Height' ), 'readonly' => false, 'option' => 'thumbnail_size_h', ), 'thumbnail_crop' => array( 'desc' => __( 'Crop thumbnail to exact dimensions' ), 'readonly' => false, 'option' => 'thumbnail_crop', ), 'medium_size_w' => array( 'desc' => __( 'Medium size image width' ), 'readonly' => false, 'option' => 'medium_size_w', ), 'medium_size_h' => array( 'desc' => __( 'Medium size image height' ), 'readonly' => false, 'option' => 'medium_size_h', ), 'medium_large_size_w' => array( 'desc' => __( 'Medium-Large size image width' ), 'readonly' => false, 'option' => 'medium_large_size_w', ), 'medium_large_size_h' => array( 'desc' => __( 'Medium-Large size image height' ), 'readonly' => false, 'option' => 'medium_large_size_h', ), 'large_size_w' => array( 'desc' => __( 'Large size image width' ), 'readonly' => false, 'option' => 'large_size_w', ), 'large_size_h' => array( 'desc' => __( 'Large size image height' ), 'readonly' => false, 'option' => 'large_size_h', ), 'default_comment_status' => array( 'desc' => __( 'Allow people to submit comments on new posts.' ), 'readonly' => false, 'option' => 'default_comment_status', ), 'default_ping_status' => array( 'desc' => __( 'Allow link notifications from other blogs (pingbacks and trackbacks) on new posts.' ), 'readonly' => false, 'option' => 'default_ping_status', ), ); /** * Filters the XML-RPC blog options property. * * @since 2.6.0 * * @param array $blog_options An array of XML-RPC blog options. */ $this->blog_options = apply_filters( 'xmlrpc_blog_options', $this->blog_options ); } /** * Retrieves the blogs of the user. * * @since 2.6.0 * * @param array $args { * Method arguments. Note: arguments must be ordered as documented. * * @type string $0 Username. * @type string $1 Password. * } * @return array|IXR_Error Array contains: * - 'isAdmin' * - 'isPrimary' - whether the blog is the user's primary blog * - 'url' * - 'blogid' * - 'blogName' * - 'xmlrpc' - url of xmlrpc endpoint */ public function wp_getUsersBlogs( $args ) { if ( ! $this->minimum_args( $args, 2 ) ) { return $this->error; } // If this isn't on WPMU then just use blogger_getUsersBlogs(). if ( ! is_multisite() ) { array_unshift( $args, 1 ); return $this->blogger_getUsersBlogs( $args ); } $this->escape( $args ); $username = $args[0]; $password = $args[1]; $user = $this->login( $username, $password ); if ( ! $user ) { return $this->error; } /** * Fires after the XML-RPC user has been authenticated but before the rest of * the method logic begins. * * All built-in XML-RPC methods use the action xmlrpc_call, with a parameter * equal to the method's name, e.g., wp.getUsersBlogs, wp.newPost, etc. * * @since 2.5.0 * @since 5.7.0 Added the `$args` and `$server` parameters. * * @param string $name The method name. * @param array|string $args The escaped arguments passed to the method. * @param wp_xmlrpc_server $server The XML-RPC server instance. */ do_action( 'xmlrpc_call', 'wp.getUsersBlogs', $args, $this ); $blogs = (array) get_blogs_of_user( $user->ID ); $struct = array(); $primary_blog_id = 0; $active_blog = get_active_blog_for_user( $user->ID ); if ( $active_blog ) { $primary_blog_id = (int) $active_blog->blog_id; } $current_network_id = get_current_network_id(); foreach ( $blogs as $blog ) { // Don't include blogs that aren't hosted at this site. if ( $blog->site_id !== $current_network_id ) { continue; } $blog_id = $blog->userblog_id; switch_to_blog( $blog_id ); $is_admin = current_user_can( 'manage_options' ); $is_primary = ( (int) $blog_id === $primary_blog_id ); $struct[] = array( 'isAdmin' => $is_admin, 'isPrimary' => $is_primary, 'url' => home_url( '/' ), 'blogid' => (string) $blog_id, 'blogName' => get_option( 'blogname' ), 'xmlrpc' => site_url( 'xmlrpc.php', 'rpc' ), ); restore_current_blog(); } return $struct; } /** * Checks if the method received at least the minimum number of arguments. * * @since 3.4.0 * * @param array $args An array of arguments to check. * @param int $count Minimum number of arguments. * @return bool True if `$args` contains at least `$count` arguments, false otherwise. */ protected function minimum_args( $args, $count ) { if ( ! is_array( $args ) || count( $args ) < $count ) { $this->error = new IXR_Error( 400, __( 'Insufficient arguments passed to this XML-RPC method.' ) ); return false; } return true; } /** * Prepares taxonomy data for return in an XML-RPC object. * * @param WP_Taxonomy $taxonomy The unprepared taxonomy data. * @param array $fields The subset of taxonomy fields to return. * @return array The prepared taxonomy data. */ protected function _prepare_taxonomy( $taxonomy, $fields ) { $_taxonomy = array( 'name' => $taxonomy->name, 'label' => $taxonomy->label, 'hierarchical' => (bool) $taxonomy->hierarchical, 'public' => (bool) $taxonomy->public, 'show_ui' => (bool) $taxonomy->show_ui, '_builtin' => (bool) $taxonomy->_builtin, ); if ( in_array( 'labels', $fields, true ) ) { $_taxonomy['labels'] = (array) $taxonomy->labels; } if ( in_array( 'cap', $fields, true ) ) { $_taxonomy['cap'] = (array) $taxonomy->cap; } if ( in_array( 'menu', $fields, true ) ) { $_taxonomy['show_in_menu'] = (bool) $taxonomy->show_in_menu; } if ( in_array( 'object_type', $fields, true ) ) { $_taxonomy['object_type'] = array_unique( (array) $taxonomy->object_type ); } /** * Filters XML-RPC-prepared data for the given taxonomy. * * @since 3.4.0 * * @param array $_taxonomy An array of taxonomy data. * @param WP_Taxonomy $taxonomy Taxonomy object. * @param array $fields The subset of taxonomy fields to return. */ return apply_filters( 'xmlrpc_prepare_taxonomy', $_taxonomy, $taxonomy, $fields ); } /** * Prepares term data for return in an XML-RPC object. * * @param array|object $term The unprepared term data. * @return array The prepared term data. */ protected function _prepare_term( $term ) { $_term = $term; if ( ! is_array( $_term ) ) { $_term = get_object_vars( $_term ); } // For integers which may be larger than XML-RPC supports ensure we return strings. $_term['term_id'] = (string) $_term['term_id']; $_term['term_group'] = (string) $_term['term_group']; $_term['term_taxonomy_id'] = (string) $_term['term_taxonomy_id']; $_term['parent'] = (string) $_term['parent']; // Count we are happy to return as an integer because people really shouldn't use terms that much. $_term['count'] = (int) $_term['count']; // Get term meta. $_term['custom_fields'] = $this->get_term_custom_fields( $_term['term_id'] ); /** * Filters XML-RPC-prepared data for the given term. * * @since 3.4.0 * * @param array $_term An array of term data. * @param array|object $term Term object or array. */ return apply_filters( 'xmlrpc_prepare_term', $_term, $term ); } /** * Converts a WordPress date string to an IXR_Date object. * * @param string $date Date string to convert. * @return IXR_Date IXR_Date object. */ protected function _convert_date( $date ) { if ( '0000-00-00 00:00:00' === $date ) { return new IXR_Date( '00000000T00:00:00Z' ); } return new IXR_Date( mysql2date( 'Ymd\TH:i:s', $date, false ) ); } /** * Converts a WordPress GMT date string to an IXR_Date object. * * @param string $date_gmt WordPress GMT date string. * @param string $date Date string. * @return IXR_Date IXR_Date object. */ protected function _convert_date_gmt( $date_gmt, $date ) { if ( '0000-00-00 00:00:00' !== $date && '0000-00-00 00:00:00' === $date_gmt ) { return new IXR_Date( get_gmt_from_date( mysql2date( 'Y-m-d H:i:s', $date, false ), 'Ymd\TH:i:s' ) ); } return $this->_convert_date( $date_gmt ); } /** * Prepares post data for return in an XML-RPC object. * * @param array $post The unprepared post data. * @param array $fields The subset of post type fields to return. * @return array The prepared post data. */ protected function _prepare_post( $post, $fields ) { // Holds the data for this post. built up based on $fields. $_post = array( 'post_id' => (string) $post['ID'] ); // Prepare common post fields. $post_fields = array( 'post_title' => $post['post_title'], 'post_date' => $this->_convert_date( $post['post_date'] ), 'post_date_gmt' => $this->_convert_date_gmt( $post['post_date_gmt'], $post['post_date'] ), 'post_modified' => $this->_convert_date( $post['post_modified'] ), 'post_modified_gmt' => $this->_convert_date_gmt( $post['post_modified_gmt'], $post['post_modified'] ), 'post_status' => $post['post_status'], 'post_type' => $post['post_type'], 'post_name' => $post['post_name'], 'post_author' => $post['post_author'], 'post_password' => $post['post_password'], 'post_excerpt' => $post['post_excerpt'], 'post_content' => $post['post_content'], 'post_parent' => (string) $post['post_parent'], 'post_mime_type' => $post['post_mime_type'], 'link' => get_permalink( $post['ID'] ), 'guid' => $post['guid'], 'menu_order' => (int) $post['menu_order'], 'comment_status' => $post['comment_status'], 'ping_status' => $post['ping_status'], 'sticky' => ( 'post' === $post['post_type'] && is_sticky( $post['ID'] ) ), ); // Thumbnail. $post_fields['post_thumbnail'] = array(); $thumbnail_id = get_post_thumbnail_id( $post['ID'] ); if ( $thumbnail_id ) { $thumbnail_size = current_theme_supports( 'post-thumbnail' ) ? 'post-thumbnail' : 'thumbnail'; $post_fields['post_thumbnail'] = $this->_prepare_media_item( get_post( $thumbnail_id ), $thumbnail_size ); } // Consider future posts as published. if ( 'future' === $post_fields['post_status'] ) { $post_fields['post_status'] = 'publish'; } // Fill in blank post format. $post_fields['post_format'] = get_post_format( $post['ID'] ); if ( empty( $post_fields['post_format'] ) ) { $post_fields['post_format'] = 'standard'; } // Merge requested $post_fields fields into $_post. if ( in_array( 'post', $fields, true ) ) { $_post = array_merge( $_post, $post_fields ); } else { $requested_fields = array_intersect_key( $post_fields, array_flip( $fields ) ); $_post = array_merge( $_post, $requested_fields ); } $all_taxonomy_fields = in_array( 'taxonomies', $fields, true ); if ( $all_taxonomy_fields || in_array( 'terms', $fields, true ) ) { $post_type_taxonomies = get_object_taxonomies( $post['post_type'], 'names' ); $terms = wp_get_object_terms( $post['ID'], $post_type_taxonomies ); $_post['terms'] = array(); foreach ( $terms as $term ) { $_post['terms'][] = $this->_prepare_term( $term ); } } if ( in_array( 'custom_fields', $fields, true ) ) { $_post['custom_fields'] = $this->get_custom_fields( $post['ID'] ); } if ( in_array( 'enclosure', $fields, true ) ) { $_post['enclosure'] = array(); $enclosures = (array) get_post_meta( $post['ID'], 'enclosure' ); if ( ! empty( $enclosures ) ) { $encdata = explode( "\n", $enclosures[0] ); $_post['enclosure']['url'] = trim( htmlspecialchars( $encdata[0] ) ); $_post['enclosure']['length'] = (int) trim( $encdata[1] ); $_post['enclosure']['type'] = trim( $encdata[2] ); } } /** * Filters XML-RPC-prepared date for the given post. * * @since 3.4.0 * * @param array $_post An array of modified post data. * @param array $post An array of post data. * @param array $fields An array of post fields. */ return apply_filters( 'xmlrpc_prepare_post', $_post, $post, $fields ); } /** * Prepares post data for return in an XML-RPC object. * * @since 3.4.0 * @since 4.6.0 Converted the `$post_type` parameter to accept a WP_Post_Type object. * * @param WP_Post_Type $post_type Post type object. * @param array $fields The subset of post fields to return. * @return array The prepared post type data. */ protected function _prepare_post_type( $post_type, $fields ) { $_post_type = array( 'name' => $post_type->name, 'label' => $post_type->label, 'hierarchical' => (bool) $post_type->hierarchical, 'public' => (bool) $post_type->public, 'show_ui' => (bool) $post_type->show_ui, '_builtin' => (bool) $post_type->_builtin, 'has_archive' => (bool) $post_type->has_archive, 'supports' => get_all_post_type_supports( $post_type->name ), ); if ( in_array( 'labels', $fields, true ) ) { $_post_type['labels'] = (array) $post_type->labels; } if ( in_array( 'cap', $fields, true ) ) { $_post_type['cap'] = (array) $post_type->cap; $_post_type['map_meta_cap'] = (bool) $post_type->map_meta_cap; } if ( in_array( 'menu', $fields, true ) ) { $_post_type['menu_position'] = (int) $post_type->menu_position; $_post_type['menu_icon'] = $post_type->menu_icon; $_post_type['show_in_menu'] = (bool) $post_type->show_in_menu; } if ( in_array( 'taxonomies', $fields, true ) ) { $_post_type['taxonomies'] = get_object_taxonomies( $post_type->name, 'names' ); } /** * Filters XML-RPC-prepared date for the given post type. * * @since 3.4.0 * @since 4.6.0 Converted the `$post_type` parameter to accept a WP_Post_Type object. * * @param array $_post_type An array of post type data. * @param WP_Post_Type $post_type Post type object. */ return apply_filters( 'xmlrpc_prepare_post_type', $_post_type, $post_type ); } /** * Prepares media item data for return in an XML-RPC object. * * @param WP_Post $media_item The unprepared media item data. * @param string $thumbnail_size The image size to use for the thumbnail URL. * @return array The prepared media item data. */ protected function _prepare_media_item( $media_item, $thumbnail_size = 'thumbnail' ) { $_media_item = array( 'attachment_id' => (string) $media_item->ID, 'date_created_gmt' => $this->_convert_date_gmt( $media_item->post_date_gmt, $media_item->post_date ), 'parent' => $media_item->post_parent, 'link' => wp_get_attachment_url( $media_item->ID ), 'title' => $media_item->post_title, 'caption' => $media_item->post_excerpt, 'description' => $media_item->post_content, 'metadata' => wp_get_attachment_metadata( $media_item->ID ), 'type' => $media_item->post_mime_type, 'alt' => get_post_meta( $media_item->ID, '_wp_attachment_image_alt', true ), ); $thumbnail_src = image_downsize( $media_item->ID, $thumbnail_size ); if ( $thumbnail_src ) { $_media_item['thumbnail'] = $thumbnail_src[0]; } else { $_media_item['thumbnail'] = $_media_item['link']; } /** * Filters XML-RPC-prepared data for the given media item. * * @since 3.4.0 * * @param array $_media_item An array of media item data. * @param WP_Post $media_item Media item object. * @param string $thumbnail_size Image size. */ return apply_filters( 'xmlrpc_prepare_media_item', $_media_item, $media_item, $thumbnail_size ); } /** * Prepares page data for return in an XML-RPC object. * * @param WP_Post $page The unprepared page data. * @return array The prepared page data. */ protected function _prepare_page( $page ) { // Get all of the page content and link. $full_page = get_extended( $page->post_content ); $link = get_permalink( $page->ID ); // Get info the page parent if there is one. $parent_title = ''; if ( ! empty( $page->post_parent ) ) { $parent = get_post( $page->post_parent ); $parent_title = $parent->post_title; } // Determine comment and ping settings. $allow_comments = comments_open( $page->ID ) ? 1 : 0; $allow_pings = pings_open( $page->ID ) ? 1 : 0; // Format page date. $page_date = $this->_convert_date( $page->post_date ); $page_date_gmt = $this->_convert_date_gmt( $page->post_date_gmt, $page->post_date ); // Pull the categories info together. $categories = array(); if ( is_object_in_taxonomy( 'page', 'category' ) ) { foreach ( wp_get_post_categories( $page->ID ) as $cat_id ) { $categories[] = get_cat_name( $cat_id ); } } // Get the author info. $author = get_userdata( $page->post_author ); $page_template = get_page_template_slug( $page->ID ); if ( empty( $page_template ) ) { $page_template = 'default'; } $_page = array( 'dateCreated' => $page_date, 'userid' => $page->post_author, 'page_id' => $page->ID, 'page_status' => $page->post_status, 'description' => $full_page['main'], 'title' => $page->post_title, 'link' => $link, 'permaLink' => $link, 'categories' => $categories, 'excerpt' => $page->post_excerpt, 'text_more' => $full_page['extended'], 'mt_allow_comments' => $allow_comments, 'mt_allow_pings' => $allow_pings, 'wp_slug' => $page->post_name, 'wp_password' => $page->post_password, 'wp_author' => $author->display_name, 'wp_page_parent_id' => $page->post_parent, 'wp_page_parent_title' => $parent_title, 'wp_page_order' => $page->menu_order, 'wp_author_id' => (string) $author->ID, 'wp_author_display_name' => $author->display_name, 'date_created_gmt' => $page_date_gmt, 'custom_fields' => $this->get_custom_fields( $page->ID ), 'wp_page_template' => $page_template, ); /** * Filters XML-RPC-prepared data for the given page. * * @since 3.4.0 * * @param array $_page An array of page data. * @param WP_Post $page Page object. */ return apply_filters( 'xmlrpc_prepare_page', $_page, $page ); } /** * Prepares comment data for return in an XML-RPC object. * * @param WP_Comment $comment The unprepared comment data. * @return array The prepared comment data. */ protected function _prepare_comment( $comment ) { // Format page date. $comment_date_gmt = $this->_convert_date_gmt( $comment->comment_date_gmt, $comment->comment_date ); if ( '0' === $comment->comment_approved ) { $comment_status = 'hold'; } elseif ( 'spam' === $comment->comment_approved ) { $comment_status = 'spam'; } elseif ( '1' === $comment->comment_approved ) { $comment_status = 'approve'; } else { $comment_status = $comment->comment_approved; } $_comment = array( 'date_created_gmt' => $comment_date_gmt, 'user_id' => $comment->user_id, 'comment_id' => $comment->comment_ID, 'parent' => $comment->comment_parent, 'status' => $comment_status, 'content' => $comment->comment_content, 'link' => get_comment_link( $comment ), 'post_id' => $comment->comment_post_ID, 'post_title' => get_the_title( $comment->comment_post_ID ), 'author' => $comment->comment_author, 'author_url' => $comment->comment_author_url, 'author_email' => $comment->comment_author_email, 'author_ip' => $comment->comment_author_IP, 'type' => $comment->comment_type, ); /** * Filters XML-RPC-prepared data for the given comment. * * @since 3.4.0 * * @param array $_comment An array of prepared comment data. * @param WP_Comment $comment Comment object. */ return apply_filters( 'xmlrpc_prepare_comment', $_comment, $comment ); } /** * Prepares user data for return in an XML-RPC object. * * @param WP_User $user The unprepared user object. * @param array $fields The subset of user fields to return. * @return array The prepared user data. */ protected function _prepare_user( $user, $fields ) { $_user = array( 'user_id' => (string) $user->ID ); $user_fields = array( 'username' => $user->user_login, 'first_name' => $user->user_firstname, 'last_name' => $user->user_lastname, 'registered' => $this->_convert_date( $user->user_registered ), 'bio' => $user->user_description, 'email' => $user->user_email, 'nickname' => $user->nickname, 'nicename' => $user->user_nicename, 'url' => $user->user_url, 'display_name' => $user->display_name, 'roles' => $user->roles, ); if ( in_array( 'all', $fields, true ) ) { $_user = array_merge( $_user, $user_fields ); } else { if ( in_array( 'basic', $fields, true ) ) { $basic_fields = array( 'username', 'email', 'registered', 'display_name', 'nicename' ); $fields = array_merge( $fields, $basic_fields ); } $requested_fields = array_intersect_key( $user_fields, array_flip( $fields ) ); $_user = array_merge( $_user, $requested_fields ); } /** * Filters XML-RPC-prepared data for the given user. * * @since 3.5.0 * * @param array $_user An array of user data. * @param WP_User $user User object. * @param array $fields An array of user fields. */ return apply_filters( 'xmlrpc_prepare_user', $_user, $user, $fields ); } /** * Creates a new post for any registered post type. * * @since 3.4.0 * * @link https://en.wikipedia.org/wiki/RSS_enclosure for information on RSS enclosures. * * @param array $args { * Method arguments. Note: top-level arguments must be ordered as documented. * * @type int $0 Blog ID (unused). * @type string $1 Username. * @type string $2 Password. * @type array $3 { * Content struct for adding a new post. See wp_insert_post() for information on * additional post fields * * @type string $post_type Post type. Default 'post'. * @type string $post_status Post status. Default 'draft' * @type string $post_title Post title. * @type int $post_author Post author ID. * @type string $post_excerpt Post excerpt. * @type string $post_content Post content. * @type string $post_date_gmt Post date in GMT. * @type string $post_date Post date. * @type string $post_password Post password (20-character limit). * @type string $comment_status Post comment enabled status. Accepts 'open' or 'closed'. * @type string $ping_status Post ping status. Accepts 'open' or 'closed'. * @type bool $sticky Whether the post should be sticky. Automatically false if * `$post_status` is 'private'. * @type int $post_thumbnail ID of an image to use as the post thumbnail/featured image. * @type array $custom_fields Array of meta key/value pairs to add to the post. * @type array $terms Associative array with taxonomy names as keys and arrays * of term IDs as values. * @type array $terms_names Associative array with taxonomy names as keys and arrays * of term names as values. * @type array $enclosure { * Array of feed enclosure data to add to post meta. * * @type string $url URL for the feed enclosure. * @type int $length Size in bytes of the enclosure. * @type string $type Mime-type for the enclosure. * } * } * } * @return int|IXR_Error Post ID on success, IXR_Error instance otherwise. */ public function wp_newPost( $args ) { if ( ! $this->minimum_args( $args, 4 ) ) { return $this->error; } $this->escape( $args ); $username = $args[1]; $password = $args[2]; $content_struct = $args[3]; $user = $this->login( $username, $password ); if ( ! $user ) { return $this->error; } // Convert the date field back to IXR form. if ( isset( $content_struct['post_date'] ) && ! ( $content_struct['post_date'] instanceof IXR_Date ) ) { $content_struct['post_date'] = $this->_convert_date( $content_struct['post_date'] ); } /* * Ignore the existing GMT date if it is empty or a non-GMT date was supplied in $content_struct, * since _insert_post() will ignore the non-GMT date if the GMT date is set. */ if ( isset( $content_struct['post_date_gmt'] ) && ! ( $content_struct['post_date_gmt'] instanceof IXR_Date ) ) { if ( '0000-00-00 00:00:00' === $content_struct['post_date_gmt'] || isset( $content_struct['post_date'] ) ) { unset( $content_struct['post_date_gmt'] ); } else { $content_struct['post_date_gmt'] = $this->_convert_date( $content_struct['post_date_gmt'] ); } } /** This action is documented in wp-includes/class-wp-xmlrpc-server.php */ do_action( 'xmlrpc_call', 'wp.newPost', $args, $this ); unset( $content_struct['ID'] ); return $this->_insert_post( $user, $content_struct ); } /** * Helper method for filtering out elements from an array. * * @since 3.4.0 * * @param int $count Number to compare to one. * @return bool True if the number is greater than one, false otherwise. */ private function _is_greater_than_one( $count ) { return $count > 1; } /** * Encapsulates the logic for sticking a post and determining if * the user has permission to do so. * * @since 4.3.0 * * @param array $post_data * @param bool $update * @return void|IXR_Error */ private function _toggle_sticky( $post_data, $update = false ) { $post_type = get_post_type_object( $post_data['post_type'] ); // Private and password-protected posts cannot be stickied. if ( 'private' === $post_data['post_status'] || ! empty( $post_data['post_password'] ) ) { // Error if the client tried to stick the post, otherwise, silently unstick. if ( ! empty( $post_data['sticky'] ) ) { return new IXR_Error( 401, __( 'Sorry, you cannot stick a private post.' ) ); } if ( $update ) { unstick_post( $post_data['ID'] ); } } elseif ( isset( $post_data['sticky'] ) ) { if ( ! current_user_can( $post_type->cap->edit_others_posts ) ) { return new IXR_Error( 401, __( 'Sorry, you are not allowed to make posts sticky.' ) ); } $sticky = wp_validate_boolean( $post_data['sticky'] ); if ( $sticky ) { stick_post( $post_data['ID'] ); } else { unstick_post( $post_data['ID'] ); } } } /** * Helper method for wp_newPost() and wp_editPost(), containing shared logic. * * @since 3.4.0 * * @see wp_insert_post() * * @param WP_User $user The post author if post_author isn't set in $content_struct. * @param array|IXR_Error $content_struct Post data to insert. * @return IXR_Error|string */ protected function _insert_post( $user, $content_struct ) { $defaults = array( 'post_status' => 'draft', 'post_type' => 'post', 'post_author' => 0, 'post_password' => '', 'post_excerpt' => '', 'post_content' => '', 'post_title' => '', 'post_date' => '', 'post_date_gmt' => '', 'post_format' => null, 'post_name' => null, 'post_thumbnail' => null, 'post_parent' => 0, 'ping_status' => '', 'comment_status' => '', 'custom_fields' => null, 'terms_names' => null, 'terms' => null, 'sticky' => null, 'enclosure' => null, 'ID' => null, ); $post_data = wp_parse_args( array_intersect_key( $content_struct, $defaults ), $defaults ); $post_type = get_post_type_object( $post_data['post_type'] ); if ( ! $post_type ) { return new IXR_Error( 403, __( 'Invalid post type.' ) ); } $update = ! empty( $post_data['ID'] ); if ( $update ) { if ( ! get_post( $post_data['ID'] ) ) { return new IXR_Error( 401, __( 'Invalid post ID.' ) ); } if ( ! current_user_can( 'edit_post', $post_data['ID'] ) ) { return new IXR_Error( 401, __( 'Sorry, you are not allowed to edit this post.' ) ); } if ( get_post_type( $post_data['ID'] ) !== $post_data['post_type'] ) { return new IXR_Error( 401, __( 'The post type may not be changed.' ) ); } } else { if ( ! current_user_can( $post_type->cap->create_posts ) || ! current_user_can( $post_type->cap->edit_posts ) ) { return new IXR_Error( 401, __( 'Sorry, you are not allowed to post on this site.' ) ); } } switch ( $post_data['post_status'] ) { case 'draft': case 'pending': break; case 'private': if ( ! current_user_can( $post_type->cap->publish_posts ) ) { return new IXR_Error( 401, __( 'Sorry, you are not allowed to create private posts in this post type.' ) ); } break; case 'publish': case 'future': if ( ! current_user_can( $post_type->cap->publish_posts ) ) { return new IXR_Error( 401, __( 'Sorry, you are not allowed to publish posts in this post type.' ) ); } break; default: if ( ! get_post_status_object( $post_data['post_status'] ) ) { $post_data['post_status'] = 'draft'; } break; } if ( ! empty( $post_data['post_password'] ) && ! current_user_can( $post_type->cap->publish_posts ) ) { return new IXR_Error( 401, __( 'Sorry, you are not allowed to create password protected posts in this post type.' ) ); } $post_data['post_author'] = absint( $post_data['post_author'] ); if ( ! empty( $post_data['post_author'] ) && $post_data['post_author'] !== $user->ID ) { if ( ! current_user_can( $post_type->cap->edit_others_posts ) ) { return new IXR_Error( 401, __( 'Sorry, you are not allowed to create posts as this user.' ) ); } $author = get_userdata( $post_data['post_author'] ); if ( ! $author ) { return new IXR_Error( 404, __( 'Invalid author ID.' ) ); } } else { $post_data['post_author'] = $user->ID; } if ( 'open' !== $post_data['comment_status'] && 'closed' !== $post_data['comment_status'] ) { unset( $post_data['comment_status'] ); } if ( 'open' !== $post_data['ping_status'] && 'closed' !== $post_data['ping_status'] ) { unset( $post_data['ping_status'] ); } // Do some timestamp voodoo. if ( ! empty( $post_data['post_date_gmt'] ) ) { // We know this is supposed to be GMT, so we're going to slap that Z on there by force. $date_created = rtrim( $post_data['post_date_gmt']->getIso(), 'Z' ) . 'Z'; } elseif ( ! empty( $post_data['post_date'] ) ) { $date_created = $post_data['post_date']->getIso(); } // Default to not flagging the post date to be edited unless it's intentional. $post_data['edit_date'] = false; if ( ! empty( $date_created ) ) { $post_data['post_date'] = iso8601_to_datetime( $date_created ); $post_data['post_date_gmt'] = iso8601_to_datetime( $date_created, 'gmt' ); // Flag the post date to be edited. $post_data['edit_date'] = true; } if ( ! isset( $post_data['ID'] ) ) { $post_data['ID'] = get_default_post_to_edit( $post_data['post_type'], true )->ID; } $post_id = $post_data['ID']; if ( 'post' === $post_data['post_type'] ) { $error = $this->_toggle_sticky( $post_data, $update ); if ( $error ) { return $error; } } if ( isset( $post_data['post_thumbnail'] ) ) { // Empty value deletes, non-empty value adds/updates. if ( ! $post_data['post_thumbnail'] ) { delete_post_thumbnail( $post_id ); } elseif ( ! get_post( absint( $post_data['post_thumbnail'] ) ) ) { return new IXR_Error( 404, __( 'Invalid attachment ID.' ) ); } set_post_thumbnail( $post_id, $post_data['post_thumbnail'] ); unset( $content_struct['post_thumbnail'] ); } if ( isset( $post_data['custom_fields'] ) ) { $this->set_custom_fields( $post_id, $post_data['custom_fields'] ); } if ( isset( $post_data['terms'] ) || isset( $post_data['terms_names'] ) ) { $post_type_taxonomies = get_object_taxonomies( $post_data['post_type'], 'objects' ); // Accumulate term IDs from terms and terms_names. $terms = array(); // First validate the terms specified by ID. if ( isset( $post_data['terms'] ) && is_array( $post_data['terms'] ) ) { $taxonomies = array_keys( $post_data['terms'] ); // Validating term IDs. foreach ( $taxonomies as $taxonomy ) { if ( ! array_key_exists( $taxonomy, $post_type_taxonomies ) ) { return new IXR_Error( 401, __( 'Sorry, one of the given taxonomies is not supported by the post type.' ) ); } if ( ! current_user_can( $post_type_taxonomies[ $taxonomy ]->cap->assign_terms ) ) { return new IXR_Error( 401, __( 'Sorry, you are not allowed to assign a term to one of the given taxonomies.' ) ); } $term_ids = $post_data['terms'][ $taxonomy ]; $terms[ $taxonomy ] = array(); foreach ( $term_ids as $term_id ) { $term = get_term_by( 'id', $term_id, $taxonomy ); if ( ! $term ) { return new IXR_Error( 403, __( 'Invalid term ID.' ) ); } $terms[ $taxonomy ][] = (int) $term_id; } } } // Now validate terms specified by name. if ( isset( $post_data['terms_names'] ) && is_array( $post_data['terms_names'] ) ) { $taxonomies = array_keys( $post_data['terms_names'] ); foreach ( $taxonomies as $taxonomy ) { if ( ! array_key_exists( $taxonomy, $post_type_taxonomies ) ) { return new IXR_Error( 401, __( 'Sorry, one of the given taxonomies is not supported by the post type.' ) ); } if ( ! current_user_can( $post_type_taxonomies[ $taxonomy ]->cap->assign_terms ) ) { return new IXR_Error( 401, __( 'Sorry, you are not allowed to assign a term to one of the given taxonomies.' ) ); } /* * For hierarchical taxonomies, we can't assign a term when multiple terms * in the hierarchy share the same name. */ $ambiguous_terms = array(); if ( is_taxonomy_hierarchical( $taxonomy ) ) { $tax_term_names = get_terms( array( 'taxonomy' => $taxonomy, 'fields' => 'names', 'hide_empty' => false, ) ); // Count the number of terms with the same name. $tax_term_names_count = array_count_values( $tax_term_names ); // Filter out non-ambiguous term names. $ambiguous_tax_term_counts = array_filter( $tax_term_names_count, array( $this, '_is_greater_than_one' ) ); $ambiguous_terms = array_keys( $ambiguous_tax_term_counts ); } $term_names = $post_data['terms_names'][ $taxonomy ]; foreach ( $term_names as $term_name ) { if ( in_array( $term_name, $ambiguous_terms, true ) ) { return new IXR_Error( 401, __( 'Ambiguous term name used in a hierarchical taxonomy. Please use term ID instead.' ) ); } $term = get_term_by( 'name', $term_name, $taxonomy ); if ( ! $term ) { // Term doesn't exist, so check that the user is allowed to create new terms. if ( ! current_user_can( $post_type_taxonomies[ $taxonomy ]->cap->edit_terms ) ) { return new IXR_Error( 401, __( 'Sorry, you are not allowed to add a term to one of the given taxonomies.' ) ); } // Create the new term. $term_info = wp_insert_term( $term_name, $taxonomy ); if ( is_wp_error( $term_info ) ) { return new IXR_Error( 500, $term_info->get_error_message() ); } $terms[ $taxonomy ][] = (int) $term_info['term_id']; } else { $terms[ $taxonomy ][] = (int) $term->term_id; } } } } $post_data['tax_input'] = $terms; unset( $post_data['terms'], $post_data['terms_names'] ); } if ( isset( $post_data['post_format'] ) ) { $format = set_post_format( $post_id, $post_data['post_format'] ); if ( is_wp_error( $format ) ) { return new IXR_Error( 500, $format->get_error_message() ); } unset( $post_data['post_format'] ); } // Handle enclosures. $enclosure = isset( $post_data['enclosure'] ) ? $post_data['enclosure'] : null; $this->add_enclosure_if_new( $post_id, $enclosure ); $this->attach_uploads( $post_id, $post_data['post_content'] ); /** * Filters post data array to be inserted via XML-RPC. * * @since 3.4.0 * * @param array $post_data Parsed array of post data. * @param array $content_struct Post data array. */ $post_data = apply_filters( 'xmlrpc_wp_insert_post_data', $post_data, $content_struct ); // Remove all null values to allow for using the insert/update post default values for those keys instead. $post_data = array_filter( $post_data, static function ( $value ) { return null !== $value; } ); $post_id = $update ? wp_update_post( $post_data, true ) : wp_insert_post( $post_data, true ); if ( is_wp_error( $post_id ) ) { return new IXR_Error( 500, $post_id->get_error_message() ); } if ( ! $post_id ) { if ( $update ) { return new IXR_Error( 401, __( 'Sorry, the post could not be updated.' ) ); } else { return new IXR_Error( 401, __( 'Sorry, the post could not be created.' ) ); } } return (string) $post_id; } /** * Edits a post for any registered post type. * * The $content_struct parameter only needs to contain fields that * should be changed. All other fields will retain their existing values. * * @since 3.4.0 * * @param array $args { * Method arguments. Note: arguments must be ordered as documented. * * @type int $0 Blog ID (unused). * @type string $1 Username. * @type string $2 Password. * @type int $3 Post ID. * @type array $4 Extra content arguments. * } * @return true|IXR_Error True on success, IXR_Error on failure. */ public function wp_editPost( $args ) { if ( ! $this->minimum_args( $args, 5 ) ) { return $this->error; } $this->escape( $args ); $username = $args[1]; $password = $args[2]; $post_id = (int) $args[3]; $content_struct = $args[4]; $user = $this->login( $username, $password ); if ( ! $user ) { return $this->error; } /** This action is documented in wp-includes/class-wp-xmlrpc-server.php */ do_action( 'xmlrpc_call', 'wp.editPost', $args, $this ); $post = get_post( $post_id, ARRAY_A ); if ( empty( $post['ID'] ) ) { return new IXR_Error( 404, __( 'Invalid post ID.' ) ); } if ( isset( $content_struct['if_not_modified_since'] ) ) { // If the post has been modified since the date provided, return an error. if ( mysql2date( 'U', $post['post_modified_gmt'] ) > $content_struct['if_not_modified_since']->getTimestamp() ) { return new IXR_Error( 409, __( 'There is a revision of this post that is more recent.' ) ); } } // Convert the date field back to IXR form. $post['post_date'] = $this->_convert_date( $post['post_date'] ); /* * Ignore the existing GMT date if it is empty or a non-GMT date was supplied in $content_struct, * since _insert_post() will ignore the non-GMT date if the GMT date is set. */ if ( '0000-00-00 00:00:00' === $post['post_date_gmt'] || isset( $content_struct['post_date'] ) ) { unset( $post['post_date_gmt'] ); } else { $post['post_date_gmt'] = $this->_convert_date( $post['post_date_gmt'] ); } /* * If the API client did not provide 'post_date', then we must not perpetuate the value that * was stored in the database, or it will appear to be an intentional edit. Conveying it here * as if it was coming from the API client will cause an otherwise zeroed out 'post_date_gmt' * to get set with the value that was originally stored in the database when the draft was created. */ if ( ! isset( $content_struct['post_date'] ) ) { unset( $post['post_date'] ); } $this->escape( $post ); $merged_content_struct = array_merge( $post, $content_struct ); $retval = $this->_insert_post( $user, $merged_content_struct ); if ( $retval instanceof IXR_Error ) { return $retval; } return true; } /** * Deletes a post for any registered post type. * * @since 3.4.0 * * @see wp_delete_post() * * @param array $args { * Method arguments. Note: arguments must be ordered as documented. * * @type int $0 Blog ID (unused). * @type string $1 Username. * @type string $2 Password. * @type int $3 Post ID. * } * @return true|IXR_Error True on success, IXR_Error instance on failure. */ public function wp_deletePost( $args ) { if ( ! $this->minimum_args( $args, 4 ) ) { return $this->error; } $this->escape( $args ); $username = $args[1]; $password = $args[2]; $post_id = (int) $args[3]; $user = $this->login( $username, $password ); if ( ! $user ) { return $this->error; } /** This action is documented in wp-includes/class-wp-xmlrpc-server.php */ do_action( 'xmlrpc_call', 'wp.deletePost', $args, $this ); $post = get_post( $post_id, ARRAY_A ); if ( empty( $post['ID'] ) ) { return new IXR_Error( 404, __( 'Invalid post ID.' ) ); } if ( ! current_user_can( 'delete_post', $post_id ) ) { return new IXR_Error( 401, __( 'Sorry, you are not allowed to delete this post.' ) ); } $result = wp_delete_post( $post_id ); if ( ! $result ) { return new IXR_Error( 500, __( 'Sorry, the post could not be deleted.' ) ); } return true; } /** * Retrieves a post. * * @since 3.4.0 * * The optional $fields parameter specifies what fields will be included * in the response array. This should be a list of field names. 'post_id' will * always be included in the response regardless of the value of $fields. * * Instead of, or in addition to, individual field names, conceptual group * names can be used to specify multiple fields. The available conceptual * groups are 'post' (all basic fields), 'taxonomies', 'custom_fields', * and 'enclosure'. * * @see get_post() * * @param array $args { * Method arguments. Note: arguments must be ordered as documented. * * @type int $0 Blog ID (unused). * @type string $1 Username. * @type string $2 Password. * @type int $3 Post ID. * @type array $4 Optional. The subset of post type fields to return. * } * @return array|IXR_Error Array contains (based on $fields parameter): * - 'post_id' * - 'post_title' * - 'post_date' * - 'post_date_gmt' * - 'post_modified' * - 'post_modified_gmt' * - 'post_status' * - 'post_type' * - 'post_name' * - 'post_author' * - 'post_password' * - 'post_excerpt' * - 'post_content' * - 'link' * - 'comment_status' * - 'ping_status' * - 'sticky' * - 'custom_fields' * - 'terms' * - 'categories' * - 'tags' * - 'enclosure' */ public function wp_getPost( $args ) { if ( ! $this->minimum_args( $args, 4 ) ) { return $this->error; } $this->escape( $args ); $username = $args[1]; $password = $args[2]; $post_id = (int) $args[3]; if ( isset( $args[4] ) ) { $fields = $args[4]; } else { /** * Filters the default post query fields used by the given XML-RPC method. * * @since 3.4.0 * * @param array $fields An array of post fields to retrieve. By default, * contains 'post', 'terms', and 'custom_fields'. * @param string $method Method name. */ $fields = apply_filters( 'xmlrpc_default_post_fields', array( 'post', 'terms', 'custom_fields' ), 'wp.getPost' ); } $user = $this->login( $username, $password ); if ( ! $user ) { return $this->error; } /** This action is documented in wp-includes/class-wp-xmlrpc-server.php */ do_action( 'xmlrpc_call', 'wp.getPost', $args, $this ); $post = get_post( $post_id, ARRAY_A ); if ( empty( $post['ID'] ) ) { return new IXR_Error( 404, __( 'Invalid post ID.' ) ); } if ( ! current_user_can( 'edit_post', $post_id ) ) { return new IXR_Error( 401, __( 'Sorry, you are not allowed to edit this post.' ) ); } return $this->_prepare_post( $post, $fields ); } /** * Retrieves posts. * * @since 3.4.0 * * @see wp_get_recent_posts() * @see wp_getPost() for more on `$fields` * @see get_posts() for more on `$filter` values * * @param array $args { * Method arguments. Note: arguments must be ordered as documented. * * @type int $0 Blog ID (unused). * @type string $1 Username. * @type string $2 Password. * @type array $3 Optional. Modifies the query used to retrieve posts. Accepts 'post_type', * 'post_status', 'number', 'offset', 'orderby', 's', and 'order'. * Default empty array. * @type array $4 Optional. The subset of post type fields to return in the response array. * } * @return array|IXR_Error Array containing a collection of posts. */ public function wp_getPosts( $args ) { if ( ! $this->minimum_args( $args, 3 ) ) { return $this->error; } $this->escape( $args ); $username = $args[1]; $password = $args[2]; $filter = isset( $args[3] ) ? $args[3] : array(); if ( isset( $args[4] ) ) { $fields = $args[4]; } else { /** This action is documented in wp-includes/class-wp-xmlrpc-server.php */ $fields = apply_filters( 'xmlrpc_default_post_fields', array( 'post', 'terms', 'custom_fields' ), 'wp.getPosts' ); } $user = $this->login( $username, $password ); if ( ! $user ) { return $this->error; } /** This action is documented in wp-includes/class-wp-xmlrpc-server.php */ do_action( 'xmlrpc_call', 'wp.getPosts', $args, $this ); $query = array(); if ( isset( $filter['post_type'] ) ) { $post_type = get_post_type_object( $filter['post_type'] ); if ( ! ( (bool) $post_type ) ) { return new IXR_Error( 403, __( 'Invalid post type.' ) ); } } else { $post_type = get_post_type_object( 'post' ); } if ( ! current_user_can( $post_type->cap->edit_posts ) ) { return new IXR_Error( 401, __( 'Sorry, you are not allowed to edit posts in this post type.' ) ); } $query['post_type'] = $post_type->name; if ( isset( $filter['post_status'] ) ) { $query['post_status'] = $filter['post_status']; } if ( isset( $filter['number'] ) ) { $query['numberposts'] = absint( $filter['number'] ); } if ( isset( $filter['offset'] ) ) { $query['offset'] = absint( $filter['offset'] ); } if ( isset( $filter['orderby'] ) ) { $query['orderby'] = $filter['orderby']; if ( isset( $filter['order'] ) ) { $query['order'] = $filter['order']; } } if ( isset( $filter['s'] ) ) { $query['s'] = $filter['s']; } $posts_list = wp_get_recent_posts( $query ); if ( ! $posts_list ) { return array(); } // Holds all the posts data. $struct = array(); foreach ( $posts_list as $post ) { if ( ! current_user_can( 'edit_post', $post['ID'] ) ) { continue; } $struct[] = $this->_prepare_post( $post, $fields ); } return $struct; } /** * Creates a new term. * * @since 3.4.0 * * @see wp_insert_term() * * @param array $args { * Method arguments. Note: arguments must be ordered as documented. * * @type int $0 Blog ID (unused). * @type string $1 Username. * @type string $2 Password. * @type array $3 Content struct for adding a new term. The struct must contain * the term 'name' and 'taxonomy'. Optional accepted values include * 'parent', 'description', and 'slug'. * } * @return int|IXR_Error The term ID on success, or an IXR_Error object on failure. */ public function wp_newTerm( $args ) { if ( ! $this->minimum_args( $args, 4 ) ) { return $this->error; } $this->escape( $args ); $username = $args[1]; $password = $args[2]; $content_struct = $args[3]; $user = $this->login( $username, $password ); if ( ! $user ) { return $this->error; } /** This action is documented in wp-includes/class-wp-xmlrpc-server.php */ do_action( 'xmlrpc_call', 'wp.newTerm', $args, $this ); if ( ! taxonomy_exists( $content_struct['taxonomy'] ) ) { return new IXR_Error( 403, __( 'Invalid taxonomy.' ) ); } $taxonomy = get_taxonomy( $content_struct['taxonomy'] ); if ( ! current_user_can( $taxonomy->cap->edit_terms ) ) { return new IXR_Error( 401, __( 'Sorry, you are not allowed to create terms in this taxonomy.' ) ); } $taxonomy = (array) $taxonomy; // Hold the data of the term. $term_data = array(); $term_data['name'] = trim( $content_struct['name'] ); if ( empty( $term_data['name'] ) ) { return new IXR_Error( 403, __( 'The term name cannot be empty.' ) ); } if ( isset( $content_struct['parent'] ) ) { if ( ! $taxonomy['hierarchical'] ) { return new IXR_Error( 403, __( 'This taxonomy is not hierarchical.' ) ); } $parent_term_id = (int) $content_struct['parent']; $parent_term = get_term( $parent_term_id, $taxonomy['name'] ); if ( is_wp_error( $parent_term ) ) { return new IXR_Error( 500, $parent_term->get_error_message() ); } if ( ! $parent_term ) { return new IXR_Error( 403, __( 'Parent term does not exist.' ) ); } $term_data['parent'] = $content_struct['parent']; } if ( isset( $content_struct['description'] ) ) { $term_data['description'] = $content_struct['description']; } if ( isset( $content_struct['slug'] ) ) { $term_data['slug'] = $content_struct['slug']; } $term = wp_insert_term( $term_data['name'], $taxonomy['name'], $term_data ); if ( is_wp_error( $term ) ) { return new IXR_Error( 500, $term->get_error_message() ); } if ( ! $term ) { return new IXR_Error( 500, __( 'Sorry, the term could not be created.' ) ); } // Add term meta. if ( isset( $content_struct['custom_fields'] ) ) { $this->set_term_custom_fields( $term['term_id'], $content_struct['custom_fields'] ); } return (string) $term['term_id']; } /** * Edits a term. * * @since 3.4.0 * * @see wp_update_term() * * @param array $args { * Method arguments. Note: arguments must be ordered as documented. * * @type int $0 Blog ID (unused). * @type string $1 Username. * @type string $2 Password. * @type int $3 Term ID. * @type array $4 Content struct for editing a term. The struct must contain the * term 'taxonomy'. Optional accepted values include 'name', 'parent', * 'description', and 'slug'. * } * @return true|IXR_Error True on success, IXR_Error instance on failure. */ public function wp_editTerm( $args ) { if ( ! $this->minimum_args( $args, 5 ) ) { return $this->error; } $this->escape( $args ); $username = $args[1]; $password = $args[2]; $term_id = (int) $args[3]; $content_struct = $args[4]; $user = $this->login( $username, $password ); if ( ! $user ) { return $this->error; } /** This action is documented in wp-includes/class-wp-xmlrpc-server.php */ do_action( 'xmlrpc_call', 'wp.editTerm', $args, $this ); if ( ! taxonomy_exists( $content_struct['taxonomy'] ) ) { return new IXR_Error( 403, __( 'Invalid taxonomy.' ) ); } $taxonomy = get_taxonomy( $content_struct['taxonomy'] ); $taxonomy = (array) $taxonomy; // Hold the data of the term. $term_data = array(); $term = get_term( $term_id, $content_struct['taxonomy'] ); if ( is_wp_error( $term ) ) { return new IXR_Error( 500, $term->get_error_message() ); } if ( ! $term ) { return new IXR_Error( 404, __( 'Invalid term ID.' ) ); } if ( ! current_user_can( 'edit_term', $term_id ) ) { return new IXR_Error( 401, __( 'Sorry, you are not allowed to edit this term.' ) ); } if ( isset( $content_struct['name'] ) ) { $term_data['name'] = trim( $content_struct['name'] ); if ( empty( $term_data['name'] ) ) { return new IXR_Error( 403, __( 'The term name cannot be empty.' ) ); } } if ( ! empty( $content_struct['parent'] ) ) { if ( ! $taxonomy['hierarchical'] ) { return new IXR_Error( 403, __( 'Cannot set parent term, taxonomy is not hierarchical.' ) ); } $parent_term_id = (int) $content_struct['parent']; $parent_term = get_term( $parent_term_id, $taxonomy['name'] ); if ( is_wp_error( $parent_term ) ) { return new IXR_Error( 500, $parent_term->get_error_message() ); } if ( ! $parent_term ) { return new IXR_Error( 403, __( 'Parent term does not exist.' ) ); } $term_data['parent'] = $content_struct['parent']; } if ( isset( $content_struct['description'] ) ) { $term_data['description'] = $content_struct['description']; } if ( isset( $content_struct['slug'] ) ) { $term_data['slug'] = $content_struct['slug']; } $term = wp_update_term( $term_id, $taxonomy['name'], $term_data ); if ( is_wp_error( $term ) ) { return new IXR_Error( 500, $term->get_error_message() ); } if ( ! $term ) { return new IXR_Error( 500, __( 'Sorry, editing the term failed.' ) ); } // Update term meta. if ( isset( $content_struct['custom_fields'] ) ) { $this->set_term_custom_fields( $term_id, $content_struct['custom_fields'] ); } return true; } /** * Deletes a term. * * @since 3.4.0 * * @see wp_delete_term() * * @param array $args { * Method arguments. Note: arguments must be ordered as documented. * * @type int $0 Blog ID (unused). * @type string $1 Username. * @type string $2 Password. * @type string $3 Taxonomy name. * @type int $4 Term ID. * } * @return true|IXR_Error True on success, IXR_Error instance on failure. */ public function wp_deleteTerm( $args ) { if ( ! $this->minimum_args( $args, 5 ) ) { return $this->error; } $this->escape( $args ); $username = $args[1]; $password = $args[2]; $taxonomy = $args[3]; $term_id = (int) $args[4]; $user = $this->login( $username, $password ); if ( ! $user ) { return $this->error; } /** This action is documented in wp-includes/class-wp-xmlrpc-server.php */ do_action( 'xmlrpc_call', 'wp.deleteTerm', $args, $this ); if ( ! taxonomy_exists( $taxonomy ) ) { return new IXR_Error( 403, __( 'Invalid taxonomy.' ) ); } $taxonomy = get_taxonomy( $taxonomy ); $term = get_term( $term_id, $taxonomy->name ); if ( is_wp_error( $term ) ) { return new IXR_Error( 500, $term->get_error_message() ); } if ( ! $term ) { return new IXR_Error( 404, __( 'Invalid term ID.' ) ); } if ( ! current_user_can( 'delete_term', $term_id ) ) { return new IXR_Error( 401, __( 'Sorry, you are not allowed to delete this term.' ) ); } $result = wp_delete_term( $term_id, $taxonomy->name ); if ( is_wp_error( $result ) ) { return new IXR_Error( 500, $result->get_error_message() ); } if ( ! $result ) { return new IXR_Error( 500, __( 'Sorry, deleting the term failed.' ) ); } return $result; } /** * Retrieves a term. * * @since 3.4.0 * * @see get_term() * * @param array $args { * Method arguments. Note: arguments must be ordered as documented. * * @type int $0 Blog ID (unused). * @type string $1 Username. * @type string $2 Password. * @type string $3 Taxonomy name. * @type int $4 Term ID. * } * @return array|IXR_Error IXR_Error on failure, array on success, containing: * - 'term_id' * - 'name' * - 'slug' * - 'term_group' * - 'term_taxonomy_id' * - 'taxonomy' * - 'description' * - 'parent' * - 'count' */ public function wp_getTerm( $args ) { if ( ! $this->minimum_args( $args, 5 ) ) { return $this->error; } $this->escape( $args ); $username = $args[1]; $password = $args[2]; $taxonomy = $args[3]; $term_id = (int) $args[4]; $user = $this->login( $username, $password ); if ( ! $user ) { return $this->error; } /** This action is documented in wp-includes/class-wp-xmlrpc-server.php */ do_action( 'xmlrpc_call', 'wp.getTerm', $args, $this ); if ( ! taxonomy_exists( $taxonomy ) ) { return new IXR_Error( 403, __( 'Invalid taxonomy.' ) ); } $taxonomy = get_taxonomy( $taxonomy ); $term = get_term( $term_id, $taxonomy->name, ARRAY_A ); if ( is_wp_error( $term ) ) { return new IXR_Error( 500, $term->get_error_message() ); } if ( ! $term ) { return new IXR_Error( 404, __( 'Invalid term ID.' ) ); } if ( ! current_user_can( 'assign_term', $term_id ) ) { return new IXR_Error( 401, __( 'Sorry, you are not allowed to assign this term.' ) ); } return $this->_prepare_term( $term ); } /** * Retrieves all terms for a taxonomy. * * @since 3.4.0 * * The optional $filter parameter modifies the query used to retrieve terms. * Accepted keys are 'number', 'offset', 'orderby', 'order', 'hide_empty', and 'search'. * * @see get_terms() * * @param array $args { * Method arguments. Note: arguments must be ordered as documented. * * @type int $0 Blog ID (unused). * @type string $1 Username. * @type string $2 Password. * @type string $3 Taxonomy name. * @type array $4 Optional. Modifies the query used to retrieve posts. Accepts 'number', * 'offset', 'orderby', 'order', 'hide_empty', and 'search'. Default empty array. * } * @return array|IXR_Error An associative array of terms data on success, IXR_Error instance otherwise. */ public function wp_getTerms( $args ) { if ( ! $this->minimum_args( $args, 4 ) ) { return $this->error; } $this->escape( $args ); $username = $args[1]; $password = $args[2]; $taxonomy = $args[3]; $filter = isset( $args[4] ) ? $args[4] : array(); $user = $this->login( $username, $password ); if ( ! $user ) { return $this->error; } /** This action is documented in wp-includes/class-wp-xmlrpc-server.php */ do_action( 'xmlrpc_call', 'wp.getTerms', $args, $this ); if ( ! taxonomy_exists( $taxonomy ) ) { return new IXR_Error( 403, __( 'Invalid taxonomy.' ) ); } $taxonomy = get_taxonomy( $taxonomy ); if ( ! current_user_can( $taxonomy->cap->assign_terms ) ) { return new IXR_Error( 401, __( 'Sorry, you are not allowed to assign terms in this taxonomy.' ) ); } $query = array( 'taxonomy' => $taxonomy->name ); if ( isset( $filter['number'] ) ) { $query['number'] = absint( $filter['number'] ); } if ( isset( $filter['offset'] ) ) { $query['offset'] = absint( $filter['offset'] ); } if ( isset( $filter['orderby'] ) ) { $query['orderby'] = $filter['orderby']; if ( isset( $filter['order'] ) ) { $query['order'] = $filter['order']; } } if ( isset( $filter['hide_empty'] ) ) { $query['hide_empty'] = $filter['hide_empty']; } else { $query['get'] = 'all'; } if ( isset( $filter['search'] ) ) { $query['search'] = $filter['search']; } $terms = get_terms( $query ); if ( is_wp_error( $terms ) ) { return new IXR_Error( 500, $terms->get_error_message() ); } $struct = array(); foreach ( $terms as $term ) { $struct[] = $this->_prepare_term( $term ); } return $struct; } /** * Retrieves a taxonomy. * * @since 3.4.0 * * @see get_taxonomy() * * @param array $args { * Method arguments. Note: arguments must be ordered as documented. * * @type int $0 Blog ID (unused). * @type string $1 Username. * @type string $2 Password. * @type string $3 Taxonomy name. * @type array $4 Optional. Array of taxonomy fields to limit to in the return. * Accepts 'labels', 'cap', 'menu', and 'object_type'. * Default empty array. * } * @return array|IXR_Error An array of taxonomy data on success, IXR_Error instance otherwise. */ public function wp_getTaxonomy( $args ) { if ( ! $this->minimum_args( $args, 4 ) ) { return $this->error; } $this->escape( $args ); $username = $args[1]; $password = $args[2]; $taxonomy = $args[3]; if ( isset( $args[4] ) ) { $fields = $args[4]; } else { /** * Filters the default taxonomy query fields used by the given XML-RPC method. * * @since 3.4.0 * * @param array $fields An array of taxonomy fields to retrieve. By default, * contains 'labels', 'cap', and 'object_type'. * @param string $method The method name. */ $fields = apply_filters( 'xmlrpc_default_taxonomy_fields', array( 'labels', 'cap', 'object_type' ), 'wp.getTaxonomy' ); } $user = $this->login( $username, $password ); if ( ! $user ) { return $this->error; } /** This action is documented in wp-includes/class-wp-xmlrpc-server.php */ do_action( 'xmlrpc_call', 'wp.getTaxonomy', $args, $this ); if ( ! taxonomy_exists( $taxonomy ) ) { return new IXR_Error( 403, __( 'Invalid taxonomy.' ) ); } $taxonomy = get_taxonomy( $taxonomy ); if ( ! current_user_can( $taxonomy->cap->assign_terms ) ) { return new IXR_Error( 401, __( 'Sorry, you are not allowed to assign terms in this taxonomy.' ) ); } return $this->_prepare_taxonomy( $taxonomy, $fields ); } /** * Retrieves all taxonomies. * * @since 3.4.0 * * @see get_taxonomies() * * @param array $args { * Method arguments. Note: arguments must be ordered as documented. * * @type int $0 Blog ID (unused). * @type string $1 Username. * @type string $2 Password. * @type array $3 Optional. An array of arguments for retrieving taxonomies. * @type array $4 Optional. The subset of taxonomy fields to return. * } * @return array|IXR_Error An associative array of taxonomy data with returned fields determined * by `$fields`, or an IXR_Error instance on failure. */ public function wp_getTaxonomies( $args ) { if ( ! $this->minimum_args( $args, 3 ) ) { return $this->error; } $this->escape( $args ); $username = $args[1]; $password = $args[2]; $filter = isset( $args[3] ) ? $args[3] : array( 'public' => true ); if ( isset( $args[4] ) ) { $fields = $args[4]; } else { /** This action is documented in wp-includes/class-wp-xmlrpc-server.php */ $fields = apply_filters( 'xmlrpc_default_taxonomy_fields', array( 'labels', 'cap', 'object_type' ), 'wp.getTaxonomies' ); } $user = $this->login( $username, $password ); if ( ! $user ) { return $this->error; } /** This action is documented in wp-includes/class-wp-xmlrpc-server.php */ do_action( 'xmlrpc_call', 'wp.getTaxonomies', $args, $this ); $taxonomies = get_taxonomies( $filter, 'objects' ); // Holds all the taxonomy data. $struct = array(); foreach ( $taxonomies as $taxonomy ) { // Capability check for post types. if ( ! current_user_can( $taxonomy->cap->assign_terms ) ) { continue; } $struct[] = $this->_prepare_taxonomy( $taxonomy, $fields ); } return $struct; } /** * Retrieves a user. * * The optional $fields parameter specifies what fields will be included * in the response array. This should be a list of field names. 'user_id' will * always be included in the response regardless of the value of $fields. * * Instead of, or in addition to, individual field names, conceptual group * names can be used to specify multiple fields. The available conceptual * groups are 'basic' and 'all'. * * @uses get_userdata() * * @param array $args { * Method arguments. Note: arguments must be ordered as documented. * * @type int $0 Blog ID (unused). * @type string $1 Username. * @type string $2 Password. * @type int $3 User ID. * @type array $4 Optional. Array of fields to return. * } * @return array|IXR_Error Array contains (based on $fields parameter): * - 'user_id' * - 'username' * - 'first_name' * - 'last_name' * - 'registered' * - 'bio' * - 'email' * - 'nickname' * - 'nicename' * - 'url' * - 'display_name' * - 'roles' */ public function wp_getUser( $args ) { if ( ! $this->minimum_args( $args, 4 ) ) { return $this->error; } $this->escape( $args ); $username = $args[1]; $password = $args[2]; $user_id = (int) $args[3]; if ( isset( $args[4] ) ) { $fields = $args[4]; } else { /** * Filters the default user query fields used by the given XML-RPC method. * * @since 3.5.0 * * @param array $fields An array of user fields to retrieve. By default, contains 'all'. * @param string $method The method name. */ $fields = apply_filters( 'xmlrpc_default_user_fields', array( 'all' ), 'wp.getUser' ); } $user = $this->login( $username, $password ); if ( ! $user ) { return $this->error; } /** This action is documented in wp-includes/class-wp-xmlrpc-server.php */ do_action( 'xmlrpc_call', 'wp.getUser', $args, $this ); if ( ! current_user_can( 'edit_user', $user_id ) ) { return new IXR_Error( 401, __( 'Sorry, you are not allowed to edit this user.' ) ); } $user_data = get_userdata( $user_id ); if ( ! $user_data ) { return new IXR_Error( 404, __( 'Invalid user ID.' ) ); } return $this->_prepare_user( $user_data, $fields ); } /** * Retrieves users. * * The optional $filter parameter modifies the query used to retrieve users. * Accepted keys are 'number' (default: 50), 'offset' (default: 0), 'role', * 'who', 'orderby', and 'order'. * * The optional $fields parameter specifies what fields will be included * in the response array. * * @uses get_users() * @see wp_getUser() for more on $fields and return values * * @param array $args { * Method arguments. Note: arguments must be ordered as documented. * * @type int $0 Blog ID (unused). * @type string $1 Username. * @type string $2 Password. * @type array $3 Optional. Arguments for the user query. * @type array $4 Optional. Fields to return. * } * @return array|IXR_Error users data */ public function wp_getUsers( $args ) { if ( ! $this->minimum_args( $args, 3 ) ) { return $this->error; } $this->escape( $args ); $username = $args[1]; $password = $args[2]; $filter = isset( $args[3] ) ? $args[3] : array(); if ( isset( $args[4] ) ) { $fields = $args[4]; } else { /** This action is documented in wp-includes/class-wp-xmlrpc-server.php */ $fields = apply_filters( 'xmlrpc_default_user_fields', array( 'all' ), 'wp.getUsers' ); } $user = $this->login( $username, $password ); if ( ! $user ) { return $this->error; } /** This action is documented in wp-includes/class-wp-xmlrpc-server.php */ do_action( 'xmlrpc_call', 'wp.getUsers', $args, $this ); if ( ! current_user_can( 'list_users' ) ) { return new IXR_Error( 401, __( 'Sorry, you are not allowed to list users.' ) ); } $query = array( 'fields' => 'all_with_meta' ); $query['number'] = ( isset( $filter['number'] ) ) ? absint( $filter['number'] ) : 50; $query['offset'] = ( isset( $filter['offset'] ) ) ? absint( $filter['offset'] ) : 0; if ( isset( $filter['orderby'] ) ) { $query['orderby'] = $filter['orderby']; if ( isset( $filter['order'] ) ) { $query['order'] = $filter['order']; } } if ( isset( $filter['role'] ) ) { if ( get_role( $filter['role'] ) === null ) { return new IXR_Error( 403, __( 'Invalid role.' ) ); } $query['role'] = $filter['role']; } if ( isset( $filter['who'] ) ) { $query['who'] = $filter['who']; } $users = get_users( $query ); $_users = array(); foreach ( $users as $user_data ) { if ( current_user_can( 'edit_user', $user_data->ID ) ) { $_users[] = $this->_prepare_user( $user_data, $fields ); } } return $_users; } /** * Retrieves information about the requesting user. * * @uses get_userdata() * * @param array $args { * Method arguments. Note: arguments must be ordered as documented. * * @type int $0 Blog ID (unused). * @type string $1 Username * @type string $2 Password * @type array $3 Optional. Fields to return. * } * @return array|IXR_Error (@see wp_getUser) */ public function wp_getProfile( $args ) { if ( ! $this->minimum_args( $args, 3 ) ) { return $this->error; } $this->escape( $args ); $username = $args[1]; $password = $args[2]; if ( isset( $args[3] ) ) { $fields = $args[3]; } else { /** This action is documented in wp-includes/class-wp-xmlrpc-server.php */ $fields = apply_filters( 'xmlrpc_default_user_fields', array( 'all' ), 'wp.getProfile' ); } $user = $this->login( $username, $password ); if ( ! $user ) { return $this->error; } /** This action is documented in wp-includes/class-wp-xmlrpc-server.php */ do_action( 'xmlrpc_call', 'wp.getProfile', $args, $this ); if ( ! current_user_can( 'edit_user', $user->ID ) ) { return new IXR_Error( 401, __( 'Sorry, you are not allowed to edit your profile.' ) ); } $user_data = get_userdata( $user->ID ); return $this->_prepare_user( $user_data, $fields ); } /** * Edits user's profile. * * @uses wp_update_user() * * @param array $args { * Method arguments. Note: arguments must be ordered as documented. * * @type int $0 Blog ID (unused). * @type string $1 Username. * @type string $2 Password. * @type array $3 Content struct. It can optionally contain: * - 'first_name' * - 'last_name' * - 'website' * - 'display_name' * - 'nickname' * - 'nicename' * - 'bio' * } * @return true|IXR_Error True, on success. */ public function wp_editProfile( $args ) { if ( ! $this->minimum_args( $args, 4 ) ) { return $this->error; } $this->escape( $args ); $username = $args[1]; $password = $args[2]; $content_struct = $args[3]; $user = $this->login( $username, $password ); if ( ! $user ) { return $this->error; } /** This action is documented in wp-includes/class-wp-xmlrpc-server.php */ do_action( 'xmlrpc_call', 'wp.editProfile', $args, $this ); if ( ! current_user_can( 'edit_user', $user->ID ) ) { return new IXR_Error( 401, __( 'Sorry, you are not allowed to edit your profile.' ) ); } // Holds data of the user. $user_data = array(); $user_data['ID'] = $user->ID; // Only set the user details if they were given. if ( isset( $content_struct['first_name'] ) ) { $user_data['first_name'] = $content_struct['first_name']; } if ( isset( $content_struct['last_name'] ) ) { $user_data['last_name'] = $content_struct['last_name']; } if ( isset( $content_struct['url'] ) ) { $user_data['user_url'] = $content_struct['url']; } if ( isset( $content_struct['display_name'] ) ) { $user_data['display_name'] = $content_struct['display_name']; } if ( isset( $content_struct['nickname'] ) ) { $user_data['nickname'] = $content_struct['nickname']; } if ( isset( $content_struct['nicename'] ) ) { $user_data['user_nicename'] = $content_struct['nicename']; } if ( isset( $content_struct['bio'] ) ) { $user_data['description'] = $content_struct['bio']; } $result = wp_update_user( $user_data ); if ( is_wp_error( $result ) ) { return new IXR_Error( 500, $result->get_error_message() ); } if ( ! $result ) { return new IXR_Error( 500, __( 'Sorry, the user could not be updated.' ) ); } return true; } /** * Retrieves a page. * * @since 2.2.0 * * @param array $args { * Method arguments. Note: arguments must be ordered as documented. * * @type int $0 Blog ID (unused). * @type int $1 Page ID. * @type string $2 Username. * @type string $3 Password. * } * @return array|IXR_Error */ public function wp_getPage( $args ) { $this->escape( $args ); $page_id = (int) $args[1]; $username = $args[2]; $password = $args[3]; $user = $this->login( $username, $password ); if ( ! $user ) { return $this->error; } $page = get_post( $page_id ); if ( ! $page ) { return new IXR_Error( 404, __( 'Invalid post ID.' ) ); } if ( ! current_user_can( 'edit_page', $page_id ) ) { return new IXR_Error( 401, __( 'Sorry, you are not allowed to edit this page.' ) ); } /** This action is documented in wp-includes/class-wp-xmlrpc-server.php */ do_action( 'xmlrpc_call', 'wp.getPage', $args, $this ); // If we found the page then format the data. if ( $page->ID && ( 'page' === $page->post_type ) ) { return $this->_prepare_page( $page ); } else { // If the page doesn't exist, indicate that. return new IXR_Error( 404, __( 'Sorry, no such page.' ) ); } } /** * Retrieves Pages. * * @since 2.2.0 * * @param array $args { * Method arguments. Note: arguments must be ordered as documented. * * @type int $0 Blog ID (unused). * @type string $1 Username. * @type string $2 Password. * @type int $3 Optional. Number of pages. Default 10. * } * @return array|IXR_Error */ public function wp_getPages( $args ) { $this->escape( $args ); $username = $args[1]; $password = $args[2]; $num_pages = isset( $args[3] ) ? (int) $args[3] : 10; $user = $this->login( $username, $password ); if ( ! $user ) { return $this->error; } if ( ! current_user_can( 'edit_pages' ) ) { return new IXR_Error( 401, __( 'Sorry, you are not allowed to edit pages.' ) ); } /** This action is documented in wp-includes/class-wp-xmlrpc-server.php */ do_action( 'xmlrpc_call', 'wp.getPages', $args, $this ); $pages = get_posts( array( 'post_type' => 'page', 'post_status' => 'any', 'numberposts' => $num_pages, ) ); $num_pages = count( $pages ); // If we have pages, put together their info. if ( $num_pages >= 1 ) { $pages_struct = array(); foreach ( $pages as $page ) { if ( current_user_can( 'edit_page', $page->ID ) ) { $pages_struct[] = $this->_prepare_page( $page ); } } return $pages_struct; } return array(); } /** * Creates a new page. * * @since 2.2.0 * * @see wp_xmlrpc_server::mw_newPost() * * @param array $args { * Method arguments. Note: arguments must be ordered as documented. * * @type int $0 Blog ID (unused). * @type string $1 Username. * @type string $2 Password. * @type array $3 Content struct. * } * @return int|IXR_Error */ public function wp_newPage( $args ) { // Items not escaped here will be escaped in wp_newPost(). $username = $this->escape( $args[1] ); $password = $this->escape( $args[2] ); $user = $this->login( $username, $password ); if ( ! $user ) { return $this->error; } /** This action is documented in wp-includes/class-wp-xmlrpc-server.php */ do_action( 'xmlrpc_call', 'wp.newPage', $args, $this ); // Mark this as content for a page. $args[3]['post_type'] = 'page'; // Let mw_newPost() do all of the heavy lifting. return $this->mw_newPost( $args ); } /** * Deletes a page. * * @since 2.2.0 * * @param array $args { * Method arguments. Note: arguments must be ordered as documented. * * @type int $0 Blog ID (unused). * @type string $1 Username. * @type string $2 Password. * @type int $3 Page ID. * } * @return true|IXR_Error True, if success. */ public function wp_deletePage( $args ) { $this->escape( $args ); $username = $args[1]; $password = $args[2]; $page_id = (int) $args[3]; $user = $this->login( $username, $password ); if ( ! $user ) { return $this->error; } /** This action is documented in wp-includes/class-wp-xmlrpc-server.php */ do_action( 'xmlrpc_call', 'wp.deletePage', $args, $this ); /* * Get the current page based on the 'page_id' and * make sure it is a page and not a post. */ $actual_page = get_post( $page_id, ARRAY_A ); if ( ! $actual_page || ( 'page' !== $actual_page['post_type'] ) ) { return new IXR_Error( 404, __( 'Sorry, no such page.' ) ); } // Make sure the user can delete pages. if ( ! current_user_can( 'delete_page', $page_id ) ) { return new IXR_Error( 401, __( 'Sorry, you are not allowed to delete this page.' ) ); } // Attempt to delete the page. $result = wp_delete_post( $page_id ); if ( ! $result ) { return new IXR_Error( 500, __( 'Failed to delete the page.' ) ); } /** * Fires after a page has been successfully deleted via XML-RPC. * * @since 3.4.0 * * @param int $page_id ID of the deleted page. * @param array $args An array of arguments to delete the page. */ do_action( 'xmlrpc_call_success_wp_deletePage', $page_id, $args ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.NotLowercase return true; } /** * Edits a page. * * @since 2.2.0 * * @param array $args { * Method arguments. Note: arguments must be ordered as documented. * * @type int $0 Blog ID (unused). * @type int $1 Page ID. * @type string $2 Username. * @type string $3 Password. * @type string $4 Content. * @type int $5 Publish flag. 0 for draft, 1 for publish. * } * @return array|IXR_Error */ public function wp_editPage( $args ) { // Items will be escaped in mw_editPost(). $page_id = (int) $args[1]; $username = $args[2]; $password = $args[3]; $content = $args[4]; $publish = $args[5]; $escaped_username = $this->escape( $username ); $escaped_password = $this->escape( $password ); $user = $this->login( $escaped_username, $escaped_password ); if ( ! $user ) { return $this->error; } /** This action is documented in wp-includes/class-wp-xmlrpc-server.php */ do_action( 'xmlrpc_call', 'wp.editPage', $args, $this ); // Get the page data and make sure it is a page. $actual_page = get_post( $page_id, ARRAY_A ); if ( ! $actual_page || ( 'page' !== $actual_page['post_type'] ) ) { return new IXR_Error( 404, __( 'Sorry, no such page.' ) ); } // Make sure the user is allowed to edit pages. if ( ! current_user_can( 'edit_page', $page_id ) ) { return new IXR_Error( 401, __( 'Sorry, you are not allowed to edit this page.' ) ); } // Mark this as content for a page. $content['post_type'] = 'page'; // Arrange args in the way mw_editPost() understands. $args = array( $page_id, $username, $password, $content, $publish, ); // Let mw_editPost() do all of the heavy lifting. return $this->mw_editPost( $args ); } /** * Retrieves page list. * * @since 2.2.0 * * @global wpdb $wpdb WordPress database abstraction object. * * @param array $args { * Method arguments. Note: arguments must be ordered as documented. * * @type int $0 Blog ID (unused). * @type string $1 Username. * @type string $2 Password. * } * @return array|IXR_Error */ public function wp_getPageList( $args ) { global $wpdb; $this->escape( $args ); $username = $args[1]; $password = $args[2]; $user = $this->login( $username, $password ); if ( ! $user ) { return $this->error; } if ( ! current_user_can( 'edit_pages' ) ) { return new IXR_Error( 401, __( 'Sorry, you are not allowed to edit pages.' ) ); } /** This action is documented in wp-includes/class-wp-xmlrpc-server.php */ do_action( 'xmlrpc_call', 'wp.getPageList', $args, $this ); // Get list of page IDs and titles. $page_list = $wpdb->get_results( " SELECT ID page_id, post_title page_title, post_parent page_parent_id, post_date_gmt, post_date, post_status FROM {$wpdb->posts} WHERE post_type = 'page' ORDER BY ID " ); // The date needs to be formatted properly. $num_pages = count( $page_list ); for ( $i = 0; $i < $num_pages; $i++ ) { $page_list[ $i ]->dateCreated = $this->_convert_date( $page_list[ $i ]->post_date ); $page_list[ $i ]->date_created_gmt = $this->_convert_date_gmt( $page_list[ $i ]->post_date_gmt, $page_list[ $i ]->post_date ); unset( $page_list[ $i ]->post_date_gmt ); unset( $page_list[ $i ]->post_date ); unset( $page_list[ $i ]->post_status ); } return $page_list; } /** * Retrieves authors list. * * @since 2.2.0 * * @param array $args { * Method arguments. Note: arguments must be ordered as documented. * * @type int $0 Blog ID (unused). * @type string $1 Username. * @type string $2 Password. * } * @return array|IXR_Error */ public function wp_getAuthors( $args ) { $this->escape( $args ); $username = $args[1]; $password = $args[2]; $user = $this->login( $username, $password ); if ( ! $user ) { return $this->error; } if ( ! current_user_can( 'edit_posts' ) ) { return new IXR_Error( 401, __( 'Sorry, you are not allowed to edit posts.' ) ); } /** This action is documented in wp-includes/class-wp-xmlrpc-server.php */ do_action( 'xmlrpc_call', 'wp.getAuthors', $args, $this ); $authors = array(); foreach ( get_users( array( 'fields' => array( 'ID', 'user_login', 'display_name' ) ) ) as $user ) { $authors[] = array( 'user_id' => $user->ID, 'user_login' => $user->user_login, 'display_name' => $user->display_name, ); } return $authors; } /** * Gets the list of all tags. * * @since 2.7.0 * * @param array $args { * Method arguments. Note: arguments must be ordered as documented. * * @type int $0 Blog ID (unused). * @type string $1 Username. * @type string $2 Password. * } * @return array|IXR_Error */ public function wp_getTags( $args ) { $this->escape( $args ); $username = $args[1]; $password = $args[2]; $user = $this->login( $username, $password ); if ( ! $user ) { return $this->error; } if ( ! current_user_can( 'edit_posts' ) ) { return new IXR_Error( 401, __( 'Sorry, you must be able to edit posts on this site in order to view tags.' ) ); } /** This action is documented in wp-includes/class-wp-xmlrpc-server.php */ do_action( 'xmlrpc_call', 'wp.getKeywords', $args, $this ); $tags = array(); $all_tags = get_tags(); if ( $all_tags ) { foreach ( (array) $all_tags as $tag ) { $struct = array(); $struct['tag_id'] = $tag->term_id; $struct['name'] = $tag->name; $struct['count'] = $tag->count; $struct['slug'] = $tag->slug; $struct['html_url'] = esc_html( get_tag_link( $tag->term_id ) ); $struct['rss_url'] = esc_html( get_tag_feed_link( $tag->term_id ) ); $tags[] = $struct; } } return $tags; } /** * Creates a new category. * * @since 2.2.0 * * @param array $args { * Method arguments. Note: arguments must be ordered as documented. * * @type int $0 Blog ID (unused). * @type string $1 Username. * @type string $2 Password. * @type array $3 Category. * } * @return int|IXR_Error Category ID. */ public function wp_newCategory( $args ) { $this->escape( $args ); $username = $args[1]; $password = $args[2]; $category = $args[3]; $user = $this->login( $username, $password ); if ( ! $user ) { return $this->error; } /** This action is documented in wp-includes/class-wp-xmlrpc-server.php */ do_action( 'xmlrpc_call', 'wp.newCategory', $args, $this ); // Make sure the user is allowed to add a category. if ( ! current_user_can( 'manage_categories' ) ) { return new IXR_Error( 401, __( 'Sorry, you are not allowed to add a category.' ) ); } /* * If no slug was provided, make it empty * so that WordPress will generate one. */ if ( empty( $category['slug'] ) ) { $category['slug'] = ''; } /* * If no parent_id was provided, make it empty * so that it will be a top-level page (no parent). */ if ( ! isset( $category['parent_id'] ) ) { $category['parent_id'] = ''; } // If no description was provided, make it empty. if ( empty( $category['description'] ) ) { $category['description'] = ''; } $new_category = array( 'cat_name' => $category['name'], 'category_nicename' => $category['slug'], 'category_parent' => $category['parent_id'], 'category_description' => $category['description'], ); $cat_id = wp_insert_category( $new_category, true ); if ( is_wp_error( $cat_id ) ) { if ( 'term_exists' === $cat_id->get_error_code() ) { return (int) $cat_id->get_error_data(); } else { return new IXR_Error( 500, __( 'Sorry, the category could not be created.' ) ); } } elseif ( ! $cat_id ) { return new IXR_Error( 500, __( 'Sorry, the category could not be created.' ) ); } /** * Fires after a new category has been successfully created via XML-RPC. * * @since 3.4.0 * * @param int $cat_id ID of the new category. * @param array $args An array of new category arguments. */ do_action( 'xmlrpc_call_success_wp_newCategory', $cat_id, $args ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.NotLowercase return $cat_id; } /** * Deletes a category. * * @since 2.5.0 * * @param array $args { * Method arguments. Note: arguments must be ordered as documented. * * @type int $0 Blog ID (unused). * @type string $1 Username. * @type string $2 Password. * @type int $3 Category ID. * } * @return bool|IXR_Error See wp_delete_term() for return info. */ public function wp_deleteCategory( $args ) { $this->escape( $args ); $username = $args[1]; $password = $args[2]; $category_id = (int) $args[3]; $user = $this->login( $username, $password ); if ( ! $user ) { return $this->error; } /** This action is documented in wp-includes/class-wp-xmlrpc-server.php */ do_action( 'xmlrpc_call', 'wp.deleteCategory', $args, $this ); if ( ! current_user_can( 'delete_term', $category_id ) ) { return new IXR_Error( 401, __( 'Sorry, you are not allowed to delete this category.' ) ); } $status = wp_delete_term( $category_id, 'category' ); if ( true === $status ) { /** * Fires after a category has been successfully deleted via XML-RPC. * * @since 3.4.0 * * @param int $category_id ID of the deleted category. * @param array $args An array of arguments to delete the category. */ do_action( 'xmlrpc_call_success_wp_deleteCategory', $category_id, $args ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.NotLowercase } return $status; } /** * Retrieves category list. * * @since 2.2.0 * * @param array $args { * Method arguments. Note: arguments must be ordered as documented. * * @type int $0 Blog ID (unused). * @type string $1 Username. * @type string $2 Password. * @type array $3 Category * @type int $4 Max number of results. * } * @return array|IXR_Error */ public function wp_suggestCategories( $args ) { $this->escape( $args ); $username = $args[1]; $password = $args[2]; $category = $args[3]; $max_results = (int) $args[4]; $user = $this->login( $username, $password ); if ( ! $user ) { return $this->error; } if ( ! current_user_can( 'edit_posts' ) ) { return new IXR_Error( 401, __( 'Sorry, you must be able to edit posts on this site in order to view categories.' ) ); } /** This action is documented in wp-includes/class-wp-xmlrpc-server.php */ do_action( 'xmlrpc_call', 'wp.suggestCategories', $args, $this ); $category_suggestions = array(); $args = array( 'get' => 'all', 'number' => $max_results, 'name__like' => $category, ); foreach ( (array) get_categories( $args ) as $cat ) { $category_suggestions[] = array( 'category_id' => $cat->term_id, 'category_name' => $cat->name, ); } return $category_suggestions; } /** * Retrieves a comment. * * @since 2.7.0 * * @param array $args { * Method arguments. Note: arguments must be ordered as documented. * * @type int $0 Blog ID (unused). * @type string $1 Username. * @type string $2 Password. * @type int $3 Comment ID. * } * @return array|IXR_Error */ public function wp_getComment( $args ) { $this->escape( $args ); $username = $args[1]; $password = $args[2]; $comment_id = (int) $args[3]; $user = $this->login( $username, $password ); if ( ! $user ) { return $this->error; } /** This action is documented in wp-includes/class-wp-xmlrpc-server.php */ do_action( 'xmlrpc_call', 'wp.getComment', $args, $this ); $comment = get_comment( $comment_id ); if ( ! $comment ) { return new IXR_Error( 404, __( 'Invalid comment ID.' ) ); } if ( ! current_user_can( 'edit_comment', $comment_id ) ) { return new IXR_Error( 403, __( 'Sorry, you are not allowed to moderate or edit this comment.' ) ); } return $this->_prepare_comment( $comment ); } /** * Retrieves comments. * * Besides the common blog_id (unused), username, and password arguments, * it takes a filter array as the last argument. * * Accepted 'filter' keys are 'status', 'post_id', 'offset', and 'number'. * * The defaults are as follows: * - 'status' - Default is ''. Filter by status (e.g., 'approve', 'hold') * - 'post_id' - Default is ''. The post where the comment is posted. * Empty string shows all comments. * - 'number' - Default is 10. Total number of media items to retrieve. * - 'offset' - Default is 0. See WP_Query::query() for more. * * @since 2.7.0 * * @param array $args { * Method arguments. Note: arguments must be ordered as documented. * * @type int $0 Blog ID (unused). * @type string $1 Username. * @type string $2 Password. * @type array $3 Optional. Query arguments. * } * @return array|IXR_Error Array containing a collection of comments. * See wp_xmlrpc_server::wp_getComment() for a description * of each item contents. */ public function wp_getComments( $args ) { $this->escape( $args ); $username = $args[1]; $password = $args[2]; $struct = isset( $args[3] ) ? $args[3] : array(); $user = $this->login( $username, $password ); if ( ! $user ) { return $this->error; } /** This action is documented in wp-includes/class-wp-xmlrpc-server.php */ do_action( 'xmlrpc_call', 'wp.getComments', $args, $this ); if ( isset( $struct['status'] ) ) { $status = $struct['status']; } else { $status = ''; } if ( ! current_user_can( 'moderate_comments' ) && 'approve' !== $status ) { return new IXR_Error( 401, __( 'Invalid comment status.' ) ); } $post_id = ''; if ( isset( $struct['post_id'] ) ) { $post_id = absint( $struct['post_id'] ); } $post_type = ''; if ( isset( $struct['post_type'] ) ) { $post_type_object = get_post_type_object( $struct['post_type'] ); if ( ! $post_type_object || ! post_type_supports( $post_type_object->name, 'comments' ) ) { return new IXR_Error( 404, __( 'Invalid post type.' ) ); } $post_type = $struct['post_type']; } $offset = 0; if ( isset( $struct['offset'] ) ) { $offset = absint( $struct['offset'] ); } $number = 10; if ( isset( $struct['number'] ) ) { $number = absint( $struct['number'] ); } $comments = get_comments( array( 'status' => $status, 'post_id' => $post_id, 'offset' => $offset, 'number' => $number, 'post_type' => $post_type, ) ); $comments_struct = array(); if ( is_array( $comments ) ) { foreach ( $comments as $comment ) { $comments_struct[] = $this->_prepare_comment( $comment ); } } return $comments_struct; } /** * Deletes a comment. * * By default, the comment will be moved to the Trash instead of deleted. * See wp_delete_comment() for more information on this behavior. * * @since 2.7.0 * * @param array $args { * Method arguments. Note: arguments must be ordered as documented. * * @type int $0 Blog ID (unused). * @type string $1 Username. * @type string $2 Password. * @type int $3 Comment ID. * } * @return bool|IXR_Error See wp_delete_comment(). */ public function wp_deleteComment( $args ) { $this->escape( $args ); $username = $args[1]; $password = $args[2]; $comment_id = (int) $args[3]; $user = $this->login( $username, $password ); if ( ! $user ) { return $this->error; } if ( ! get_comment( $comment_id ) ) { return new IXR_Error( 404, __( 'Invalid comment ID.' ) ); } if ( ! current_user_can( 'edit_comment', $comment_id ) ) { return new IXR_Error( 403, __( 'Sorry, you are not allowed to delete this comment.' ) ); } /** This action is documented in wp-includes/class-wp-xmlrpc-server.php */ do_action( 'xmlrpc_call', 'wp.deleteComment', $args, $this ); $status = wp_delete_comment( $comment_id ); if ( true === $status ) { /** * Fires after a comment has been successfully deleted via XML-RPC. * * @since 3.4.0 * * @param int $comment_id ID of the deleted comment. * @param array $args An array of arguments to delete the comment. */ do_action( 'xmlrpc_call_success_wp_deleteComment', $comment_id, $args ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.NotLowercase } return $status; } /** * Edits a comment. * * Besides the common blog_id (unused), username, and password arguments, * it takes a comment_id integer and a content_struct array as the last argument. * * The allowed keys in the content_struct array are: * - 'author' * - 'author_url' * - 'author_email' * - 'content' * - 'date_created_gmt' * - 'status'. Common statuses are 'approve', 'hold', 'spam'. See get_comment_statuses() for more details. * * @since 2.7.0 * * @param array $args { * Method arguments. Note: arguments must be ordered as documented. * * @type int $0 Blog ID (unused). * @type string $1 Username. * @type string $2 Password. * @type int $3 Comment ID. * @type array $4 Content structure. * } * @return true|IXR_Error True, on success. */ public function wp_editComment( $args ) { $this->escape( $args ); $username = $args[1]; $password = $args[2]; $comment_id = (int) $args[3]; $content_struct = $args[4]; $user = $this->login( $username, $password ); if ( ! $user ) { return $this->error; } if ( ! get_comment( $comment_id ) ) { return new IXR_Error( 404, __( 'Invalid comment ID.' ) ); } if ( ! current_user_can( 'edit_comment', $comment_id ) ) { return new IXR_Error( 403, __( 'Sorry, you are not allowed to moderate or edit this comment.' ) ); } /** This action is documented in wp-includes/class-wp-xmlrpc-server.php */ do_action( 'xmlrpc_call', 'wp.editComment', $args, $this ); $comment = array( 'comment_ID' => $comment_id, ); if ( isset( $content_struct['status'] ) ) { $statuses = get_comment_statuses(); $statuses = array_keys( $statuses ); if ( ! in_array( $content_struct['status'], $statuses, true ) ) { return new IXR_Error( 401, __( 'Invalid comment status.' ) ); } $comment['comment_approved'] = $content_struct['status']; } // Do some timestamp voodoo. if ( ! empty( $content_struct['date_created_gmt'] ) ) { // We know this is supposed to be GMT, so we're going to slap that Z on there by force. $date_created = rtrim( $content_struct['date_created_gmt']->getIso(), 'Z' ) . 'Z'; $comment['comment_date'] = get_date_from_gmt( $date_created ); $comment['comment_date_gmt'] = iso8601_to_datetime( $date_created, 'gmt' ); } if ( isset( $content_struct['content'] ) ) { $comment['comment_content'] = $content_struct['content']; } if ( isset( $content_struct['author'] ) ) { $comment['comment_author'] = $content_struct['author']; } if ( isset( $content_struct['author_url'] ) ) { $comment['comment_author_url'] = $content_struct['author_url']; } if ( isset( $content_struct['author_email'] ) ) { $comment['comment_author_email'] = $content_struct['author_email']; } $result = wp_update_comment( $comment, true ); if ( is_wp_error( $result ) ) { return new IXR_Error( 500, $result->get_error_message() ); } if ( ! $result ) { return new IXR_Error( 500, __( 'Sorry, the comment could not be updated.' ) ); } /** * Fires after a comment has been successfully updated via XML-RPC. * * @since 3.4.0 * * @param int $comment_id ID of the updated comment. * @param array $args An array of arguments to update the comment. */ do_action( 'xmlrpc_call_success_wp_editComment', $comment_id, $args ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.NotLowercase return true; } /** * Creates a new comment. * * @since 2.7.0 * * @param array $args { * Method arguments. Note: arguments must be ordered as documented. * * @type int $0 Blog ID (unused). * @type string $1 Username. * @type string $2 Password. * @type string|int $3 Post ID or URL. * @type array $4 Content structure. * } * @return int|IXR_Error See wp_new_comment(). */ public function wp_newComment( $args ) { $this->escape( $args ); $username = $args[1]; $password = $args[2]; $post = $args[3]; $content_struct = $args[4]; /** * Filters whether to allow anonymous comments over XML-RPC. * * @since 2.7.0 * * @param bool $allow Whether to allow anonymous commenting via XML-RPC. * Default false. */ $allow_anon = apply_filters( 'xmlrpc_allow_anonymous_comments', false ); $user = $this->login( $username, $password ); if ( ! $user ) { $logged_in = false; if ( $allow_anon && get_option( 'comment_registration' ) ) { return new IXR_Error( 403, __( 'Sorry, you must be logged in to comment.' ) ); } elseif ( ! $allow_anon ) { return $this->error; } } else { $logged_in = true; } if ( is_numeric( $post ) ) { $post_id = absint( $post ); } else { $post_id = url_to_postid( $post ); } if ( ! $post_id ) { return new IXR_Error( 404, __( 'Invalid post ID.' ) ); } if ( ! get_post( $post_id ) ) { return new IXR_Error( 404, __( 'Invalid post ID.' ) ); } if ( ! comments_open( $post_id ) ) { return new IXR_Error( 403, __( 'Sorry, comments are closed for this item.' ) ); } if ( 'publish' === get_post_status( $post_id ) && ! current_user_can( 'edit_post', $post_id ) && post_password_required( $post_id ) ) { return new IXR_Error( 403, __( 'Sorry, you are not allowed to comment on this post.' ) ); } if ( 'private' === get_post_status( $post_id ) && ! current_user_can( 'read_post', $post_id ) ) { return new IXR_Error( 403, __( 'Sorry, you are not allowed to comment on this post.' ) ); } $comment = array( 'comment_post_ID' => $post_id, 'comment_content' => trim( $content_struct['content'] ), ); if ( $logged_in ) { $display_name = $user->display_name; $user_email = $user->user_email; $user_url = $user->user_url; $comment['comment_author'] = $this->escape( $display_name ); $comment['comment_author_email'] = $this->escape( $user_email ); $comment['comment_author_url'] = $this->escape( $user_url ); $comment['user_id'] = $user->ID; } else { $comment['comment_author'] = ''; if ( isset( $content_struct['author'] ) ) { $comment['comment_author'] = $content_struct['author']; } $comment['comment_author_email'] = ''; if ( isset( $content_struct['author_email'] ) ) { $comment['comment_author_email'] = $content_struct['author_email']; } $comment['comment_author_url'] = ''; if ( isset( $content_struct['author_url'] ) ) { $comment['comment_author_url'] = $content_struct['author_url']; } $comment['user_id'] = 0; if ( get_option( 'require_name_email' ) ) { if ( strlen( $comment['comment_author_email'] ) < 6 || '' === $comment['comment_author'] ) { return new IXR_Error( 403, __( 'Comment author name and email are required.' ) ); } elseif ( ! is_email( $comment['comment_author_email'] ) ) { return new IXR_Error( 403, __( 'A valid email address is required.' ) ); } } } $comment['comment_parent'] = isset( $content_struct['comment_parent'] ) ? absint( $content_struct['comment_parent'] ) : 0; /** This filter is documented in wp-includes/comment.php */ $allow_empty = apply_filters( 'allow_empty_comment', false, $comment ); if ( ! $allow_empty && '' === $comment['comment_content'] ) { return new IXR_Error( 403, __( 'Comment is required.' ) ); } /** This action is documented in wp-includes/class-wp-xmlrpc-server.php */ do_action( 'xmlrpc_call', 'wp.newComment', $args, $this ); $comment_id = wp_new_comment( $comment, true ); if ( is_wp_error( $comment_id ) ) { return new IXR_Error( 403, $comment_id->get_error_message() ); } if ( ! $comment_id ) { return new IXR_Error( 403, __( 'An error occurred while processing your comment. Please ensure all fields are filled correctly and try again.' ) ); } /** * Fires after a new comment has been successfully created via XML-RPC. * * @since 3.4.0 * * @param int $comment_id ID of the new comment. * @param array $args An array of new comment arguments. */ do_action( 'xmlrpc_call_success_wp_newComment', $comment_id, $args ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.NotLowercase return $comment_id; } /** * Retrieves all of the comment status. * * @since 2.7.0 * * @param array $args { * Method arguments. Note: arguments must be ordered as documented. * * @type int $0 Blog ID (unused). * @type string $1 Username. * @type string $2 Password. * } * @return array|IXR_Error */ public function wp_getCommentStatusList( $args ) { $this->escape( $args ); $username = $args[1]; $password = $args[2]; $user = $this->login( $username, $password ); if ( ! $user ) { return $this->error; } if ( ! current_user_can( 'publish_posts' ) ) { return new IXR_Error( 403, __( 'Sorry, you are not allowed to access details about this site.' ) ); } /** This action is documented in wp-includes/class-wp-xmlrpc-server.php */ do_action( 'xmlrpc_call', 'wp.getCommentStatusList', $args, $this ); return get_comment_statuses(); } /** * Retrieves comment counts. * * @since 2.5.0 * * @param array $args { * Method arguments. Note: arguments must be ordered as documented. * * @type int $0 Blog ID (unused). * @type string $1 Username. * @type string $2 Password. * @type int $3 Post ID. * } * @return array|IXR_Error */ public function wp_getCommentCount( $args ) { $this->escape( $args ); $username = $args[1]; $password = $args[2]; $post_id = (int) $args[3]; $user = $this->login( $username, $password ); if ( ! $user ) { return $this->error; } $post = get_post( $post_id, ARRAY_A ); if ( empty( $post['ID'] ) ) { return new IXR_Error( 404, __( 'Invalid post ID.' ) ); } if ( ! current_user_can( 'edit_post', $post_id ) ) { return new IXR_Error( 403, __( 'Sorry, you are not allowed to access details of this post.' ) ); } /** This action is documented in wp-includes/class-wp-xmlrpc-server.php */ do_action( 'xmlrpc_call', 'wp.getCommentCount', $args, $this ); $count = wp_count_comments( $post_id ); return array( 'approved' => $count->approved, 'awaiting_moderation' => $count->moderated, 'spam' => $count->spam, 'total_comments' => $count->total_comments, ); } /** * Retrieves post statuses. * * @since 2.5.0 * * @param array $args { * Method arguments. Note: arguments must be ordered as documented. * * @type int $0 Blog ID (unused). * @type string $1 Username. * @type string $2 Password. * } * @return array|IXR_Error */ public function wp_getPostStatusList( $args ) { $this->escape( $args ); $username = $args[1]; $password = $args[2]; $user = $this->login( $username, $password ); if ( ! $user ) { return $this->error; } if ( ! current_user_can( 'edit_posts' ) ) { return new IXR_Error( 403, __( 'Sorry, you are not allowed to access details about this site.' ) ); } /** This action is documented in wp-includes/class-wp-xmlrpc-server.php */ do_action( 'xmlrpc_call', 'wp.getPostStatusList', $args, $this ); return get_post_statuses(); } /** * Retrieves page statuses. * * @since 2.5.0 * * @param array $args { * Method arguments. Note: arguments must be ordered as documented. * * @type int $0 Blog ID (unused). * @type string $1 Username. * @type string $2 Password. * } * @return array|IXR_Error */ public function wp_getPageStatusList( $args ) { $this->escape( $args ); $username = $args[1]; $password = $args[2]; $user = $this->login( $username, $password ); if ( ! $user ) { return $this->error; } if ( ! current_user_can( 'edit_pages' ) ) { return new IXR_Error( 403, __( 'Sorry, you are not allowed to access details about this site.' ) ); } /** This action is documented in wp-includes/class-wp-xmlrpc-server.php */ do_action( 'xmlrpc_call', 'wp.getPageStatusList', $args, $this ); return get_page_statuses(); } /** * Retrieves page templates. * * @since 2.6.0 * * @param array $args { * Method arguments. Note: arguments must be ordered as documented. * * @type int $0 Blog ID (unused). * @type string $1 Username. * @type string $2 Password. * } * @return array|IXR_Error */ public function wp_getPageTemplates( $args ) { $this->escape( $args ); $username = $args[1]; $password = $args[2]; $user = $this->login( $username, $password ); if ( ! $user ) { return $this->error; } if ( ! current_user_can( 'edit_pages' ) ) { return new IXR_Error( 403, __( 'Sorry, you are not allowed to access details about this site.' ) ); } $templates = get_page_templates(); $templates['Default'] = 'default'; return $templates; } /** * Retrieves blog options. * * @since 2.6.0 * * @param array $args { * Method arguments. Note: arguments must be ordered as documented. * * @type int $0 Blog ID (unused). * @type string $1 Username. * @type string $2 Password. * @type array $3 Optional. Options. * } * @return array|IXR_Error */ public function wp_getOptions( $args ) { $this->escape( $args ); $username = $args[1]; $password = $args[2]; $options = isset( $args[3] ) ? (array) $args[3] : array(); $user = $this->login( $username, $password ); if ( ! $user ) { return $this->error; } // If no specific options where asked for, return all of them. if ( count( $options ) === 0 ) { $options = array_keys( $this->blog_options ); } return $this->_getOptions( $options ); } /** * Retrieves blog options value from list. * * @since 2.6.0 * * @param array $options Options to retrieve. * @return array */ public function _getOptions( $options ) { $data = array(); $can_manage = current_user_can( 'manage_options' ); foreach ( $options as $option ) { if ( array_key_exists( $option, $this->blog_options ) ) { $data[ $option ] = $this->blog_options[ $option ]; // Is the value static or dynamic? if ( isset( $data[ $option ]['option'] ) ) { $data[ $option ]['value'] = get_option( $data[ $option ]['option'] ); unset( $data[ $option ]['option'] ); } if ( ! $can_manage ) { $data[ $option ]['readonly'] = true; } } } return $data; } /** * Updates blog options. * * @since 2.6.0 * * @param array $args { * Method arguments. Note: arguments must be ordered as documented. * * @type int $0 Blog ID (unused). * @type string $1 Username. * @type string $2 Password. * @type array $3 Options. * } * @return array|IXR_Error */ public function wp_setOptions( $args ) { $this->escape( $args ); $username = $args[1]; $password = $args[2]; $options = (array) $args[3]; $user = $this->login( $username, $password ); if ( ! $user ) { return $this->error; } if ( ! current_user_can( 'manage_options' ) ) { return new IXR_Error( 403, __( 'Sorry, you are not allowed to update options.' ) ); } $option_names = array(); foreach ( $options as $o_name => $o_value ) { $option_names[] = $o_name; if ( ! array_key_exists( $o_name, $this->blog_options ) ) { continue; } if ( $this->blog_options[ $o_name ]['readonly'] ) { continue; } update_option( $this->blog_options[ $o_name ]['option'], wp_unslash( $o_value ) ); } // Now return the updated values. return $this->_getOptions( $option_names ); } /** * Retrieves a media item by ID. * * @since 3.1.0 * * @param array $args { * Method arguments. Note: arguments must be ordered as documented. * * @type int $0 Blog ID (unused). * @type string $1 Username. * @type string $2 Password. * @type int $3 Attachment ID. * } * @return array|IXR_Error Associative array contains: * - 'date_created_gmt' * - 'parent' * - 'link' * - 'thumbnail' * - 'title' * - 'caption' * - 'description' * - 'metadata' */ public function wp_getMediaItem( $args ) { $this->escape( $args ); $username = $args[1]; $password = $args[2]; $attachment_id = (int) $args[3]; $user = $this->login( $username, $password ); if ( ! $user ) { return $this->error; } if ( ! current_user_can( 'upload_files' ) ) { return new IXR_Error( 403, __( 'Sorry, you are not allowed to upload files.' ) ); } /** This action is documented in wp-includes/class-wp-xmlrpc-server.php */ do_action( 'xmlrpc_call', 'wp.getMediaItem', $args, $this ); $attachment = get_post( $attachment_id ); if ( ! $attachment || 'attachment' !== $attachment->post_type ) { return new IXR_Error( 404, __( 'Invalid attachment ID.' ) ); } return $this->_prepare_media_item( $attachment ); } /** * Retrieves a collection of media library items (or attachments). * * Besides the common blog_id (unused), username, and password arguments, * it takes a filter array as the last argument. * * Accepted 'filter' keys are 'parent_id', 'mime_type', 'offset', and 'number'. * * The defaults are as follows: * - 'number' - Default is 5. Total number of media items to retrieve. * - 'offset' - Default is 0. See WP_Query::query() for more. * - 'parent_id' - Default is ''. The post where the media item is attached. * Empty string shows all media items. 0 shows unattached media items. * - 'mime_type' - Default is ''. Filter by mime type (e.g., 'image/jpeg', 'application/pdf') * * @since 3.1.0 * * @param array $args { * Method arguments. Note: arguments must be ordered as documented. * * @type int $0 Blog ID (unused). * @type string $1 Username. * @type string $2 Password. * @type array $3 Optional. Query arguments. * } * @return array|IXR_Error Array containing a collection of media items. * See wp_xmlrpc_server::wp_getMediaItem() for a description * of each item contents. */ public function wp_getMediaLibrary( $args ) { $this->escape( $args ); $username = $args[1]; $password = $args[2]; $struct = isset( $args[3] ) ? $args[3] : array(); $user = $this->login( $username, $password ); if ( ! $user ) { return $this->error; } if ( ! current_user_can( 'upload_files' ) ) { return new IXR_Error( 401, __( 'Sorry, you are not allowed to upload files.' ) ); } /** This action is documented in wp-includes/class-wp-xmlrpc-server.php */ do_action( 'xmlrpc_call', 'wp.getMediaLibrary', $args, $this ); $parent_id = ( isset( $struct['parent_id'] ) ) ? absint( $struct['parent_id'] ) : ''; $mime_type = ( isset( $struct['mime_type'] ) ) ? $struct['mime_type'] : ''; $offset = ( isset( $struct['offset'] ) ) ? absint( $struct['offset'] ) : 0; $number = ( isset( $struct['number'] ) ) ? absint( $struct['number'] ) : -1; $attachments = get_posts( array( 'post_type' => 'attachment', 'post_parent' => $parent_id, 'offset' => $offset, 'numberposts' => $number, 'post_mime_type' => $mime_type, ) ); $attachments_struct = array(); foreach ( $attachments as $attachment ) { $attachments_struct[] = $this->_prepare_media_item( $attachment ); } return $attachments_struct; } /** * Retrieves a list of post formats used by the site. * * @since 3.1.0 * * @param array $args { * Method arguments. Note: arguments must be ordered as documented. * * @type int $0 Blog ID (unused). * @type string $1 Username. * @type string $2 Password. * } * @return array|IXR_Error List of post formats, otherwise IXR_Error object. */ public function wp_getPostFormats( $args ) { $this->escape( $args ); $username = $args[1]; $password = $args[2]; $user = $this->login( $username, $password ); if ( ! $user ) { return $this->error; } if ( ! current_user_can( 'edit_posts' ) ) { return new IXR_Error( 403, __( 'Sorry, you are not allowed to access details about this site.' ) ); } /** This action is documented in wp-includes/class-wp-xmlrpc-server.php */ do_action( 'xmlrpc_call', 'wp.getPostFormats', $args, $this ); $formats = get_post_format_strings(); // Find out if they want a list of currently supports formats. if ( isset( $args[3] ) && is_array( $args[3] ) ) { if ( $args[3]['show-supported'] ) { if ( current_theme_supports( 'post-formats' ) ) { $supported = get_theme_support( 'post-formats' ); $data = array(); $data['all'] = $formats; $data['supported'] = $supported[0]; $formats = $data; } } } return $formats; } /** * Retrieves a post type. * * @since 3.4.0 * * @see get_post_type_object() * * @param array $args { * Method arguments. Note: arguments must be ordered as documented. * * @type int $0 Blog ID (unused). * @type string $1 Username. * @type string $2 Password. * @type string $3 Post type name. * @type array $4 Optional. Fields to fetch. * } * @return array|IXR_Error Array contains: * - 'labels' * - 'description' * - 'capability_type' * - 'cap' * - 'map_meta_cap' * - 'hierarchical' * - 'menu_position' * - 'taxonomies' * - 'supports' */ public function wp_getPostType( $args ) { if ( ! $this->minimum_args( $args, 4 ) ) { return $this->error; } $this->escape( $args ); $username = $args[1]; $password = $args[2]; $post_type_name = $args[3]; if ( isset( $args[4] ) ) { $fields = $args[4]; } else { /** * Filters the default post type query fields used by the given XML-RPC method. * * @since 3.4.0 * * @param array $fields An array of post type fields to retrieve. By default, * contains 'labels', 'cap', and 'taxonomies'. * @param string $method The method name. */ $fields = apply_filters( 'xmlrpc_default_posttype_fields', array( 'labels', 'cap', 'taxonomies' ), 'wp.getPostType' ); } $user = $this->login( $username, $password ); if ( ! $user ) { return $this->error; } /** This action is documented in wp-includes/class-wp-xmlrpc-server.php */ do_action( 'xmlrpc_call', 'wp.getPostType', $args, $this ); if ( ! post_type_exists( $post_type_name ) ) { return new IXR_Error( 403, __( 'Invalid post type.' ) ); } $post_type = get_post_type_object( $post_type_name ); if ( ! current_user_can( $post_type->cap->edit_posts ) ) { return new IXR_Error( 401, __( 'Sorry, you are not allowed to edit posts in this post type.' ) ); } return $this->_prepare_post_type( $post_type, $fields ); } /** * Retrieves post types. * * @since 3.4.0 * * @see get_post_types() * * @param array $args { * Method arguments. Note: arguments must be ordered as documented. * * @type int $0 Blog ID (unused). * @type string $1 Username. * @type string $2 Password. * @type array $3 Optional. Query arguments. * @type array $4 Optional. Fields to fetch. * } * @return array|IXR_Error */ public function wp_getPostTypes( $args ) { if ( ! $this->minimum_args( $args, 3 ) ) { return $this->error; } $this->escape( $args ); $username = $args[1]; $password = $args[2]; $filter = isset( $args[3] ) ? $args[3] : array( 'public' => true ); if ( isset( $args[4] ) ) { $fields = $args[4]; } else { /** This action is documented in wp-includes/class-wp-xmlrpc-server.php */ $fields = apply_filters( 'xmlrpc_default_posttype_fields', array( 'labels', 'cap', 'taxonomies' ), 'wp.getPostTypes' ); } $user = $this->login( $username, $password ); if ( ! $user ) { return $this->error; } /** This action is documented in wp-includes/class-wp-xmlrpc-server.php */ do_action( 'xmlrpc_call', 'wp.getPostTypes', $args, $this ); $post_types = get_post_types( $filter, 'objects' ); $struct = array(); foreach ( $post_types as $post_type ) { if ( ! current_user_can( $post_type->cap->edit_posts ) ) { continue; } $struct[ $post_type->name ] = $this->_prepare_post_type( $post_type, $fields ); } return $struct; } /** * Retrieves revisions for a specific post. * * @since 3.5.0 * * The optional $fields parameter specifies what fields will be included * in the response array. * * @uses wp_get_post_revisions() * @see wp_getPost() for more on $fields * * @param array $args { * Method arguments. Note: arguments must be ordered as documented. * * @type int $0 Blog ID (unused). * @type string $1 Username. * @type string $2 Password. * @type int $3 Post ID. * @type array $4 Optional. Fields to fetch. * } * @return array|IXR_Error Array containing a collection of posts. */ public function wp_getRevisions( $args ) { if ( ! $this->minimum_args( $args, 4 ) ) { return $this->error; } $this->escape( $args ); $username = $args[1]; $password = $args[2]; $post_id = (int) $args[3]; if ( isset( $args[4] ) ) { $fields = $args[4]; } else { /** * Filters the default revision query fields used by the given XML-RPC method. * * @since 3.5.0 * * @param array $field An array of revision fields to retrieve. By default, * contains 'post_date' and 'post_date_gmt'. * @param string $method The method name. */ $fields = apply_filters( 'xmlrpc_default_revision_fields', array( 'post_date', 'post_date_gmt' ), 'wp.getRevisions' ); } $user = $this->login( $username, $password ); if ( ! $user ) { return $this->error; } /** This action is documented in wp-includes/class-wp-xmlrpc-server.php */ do_action( 'xmlrpc_call', 'wp.getRevisions', $args, $this ); $post = get_post( $post_id ); if ( ! $post ) { return new IXR_Error( 404, __( 'Invalid post ID.' ) ); } if ( ! current_user_can( 'edit_post', $post_id ) ) { return new IXR_Error( 401, __( 'Sorry, you are not allowed to edit posts.' ) ); } // Check if revisions are enabled. if ( ! wp_revisions_enabled( $post ) ) { return new IXR_Error( 401, __( 'Sorry, revisions are disabled.' ) ); } $revisions = wp_get_post_revisions( $post_id ); if ( ! $revisions ) { return array(); } $struct = array(); foreach ( $revisions as $revision ) { if ( ! current_user_can( 'read_post', $revision->ID ) ) { continue; } // Skip autosaves. if ( wp_is_post_autosave( $revision ) ) { continue; } $struct[] = $this->_prepare_post( get_object_vars( $revision ), $fields ); } return $struct; } /** * Restores a post revision. * * @since 3.5.0 * * @uses wp_restore_post_revision() * * @param array $args { * Method arguments. Note: arguments must be ordered as documented. * * @type int $0 Blog ID (unused). * @type string $1 Username. * @type string $2 Password. * @type int $3 Revision ID. * } * @return bool|IXR_Error false if there was an error restoring, true if success. */ public function wp_restoreRevision( $args ) { if ( ! $this->minimum_args( $args, 3 ) ) { return $this->error; } $this->escape( $args ); $username = $args[1]; $password = $args[2]; $revision_id = (int) $args[3]; $user = $this->login( $username, $password ); if ( ! $user ) { return $this->error; } /** This action is documented in wp-includes/class-wp-xmlrpc-server.php */ do_action( 'xmlrpc_call', 'wp.restoreRevision', $args, $this ); $revision = wp_get_post_revision( $revision_id ); if ( ! $revision ) { return new IXR_Error( 404, __( 'Invalid post ID.' ) ); } if ( wp_is_post_autosave( $revision ) ) { return new IXR_Error( 404, __( 'Invalid post ID.' ) ); } $post = get_post( $revision->post_parent ); if ( ! $post ) { return new IXR_Error( 404, __( 'Invalid post ID.' ) ); } if ( ! current_user_can( 'edit_post', $revision->post_parent ) ) { return new IXR_Error( 401, __( 'Sorry, you are not allowed to edit this post.' ) ); } // Check if revisions are disabled. if ( ! wp_revisions_enabled( $post ) ) { return new IXR_Error( 401, __( 'Sorry, revisions are disabled.' ) ); } $post = wp_restore_post_revision( $revision_id ); return (bool) $post; } /* * Blogger API functions. * Specs on http://plant.blogger.com/api and https://groups.yahoo.com/group/bloggerDev/ */ /** * Retrieves blogs that user owns. * * Will make more sense once we support multiple blogs. * * @since 1.5.0 * * @param array $args { * Method arguments. Note: arguments must be ordered as documented. * * @type int $0 Blog ID (unused). * @type string $1 Username. * @type string $2 Password. * } * @return array|IXR_Error */ public function blogger_getUsersBlogs( $args ) { if ( ! $this->minimum_args( $args, 3 ) ) { return $this->error; } if ( is_multisite() ) { return $this->_multisite_getUsersBlogs( $args ); } $this->escape( $args ); $username = $args[1]; $password = $args[2]; $user = $this->login( $username, $password ); if ( ! $user ) { return $this->error; } /** This action is documented in wp-includes/class-wp-xmlrpc-server.php */ do_action( 'xmlrpc_call', 'blogger.getUsersBlogs', $args, $this ); $is_admin = current_user_can( 'manage_options' ); $struct = array( 'isAdmin' => $is_admin, 'url' => get_option( 'home' ) . '/', 'blogid' => '1', 'blogName' => get_option( 'blogname' ), 'xmlrpc' => site_url( 'xmlrpc.php', 'rpc' ), ); return array( $struct ); } /** * Private function for retrieving a users blogs for multisite setups. * * @since 3.0.0 * * @param array $args { * Method arguments. Note: arguments must be ordered as documented. * * @type int $0 Blog ID (unused). * @type string $1 Username. * @type string $2 Password. * } * @return array|IXR_Error */ protected function _multisite_getUsersBlogs( $args ) { $current_blog = get_site(); $domain = $current_blog->domain; $path = $current_blog->path . 'xmlrpc.php'; $blogs = $this->wp_getUsersBlogs( $args ); if ( $blogs instanceof IXR_Error ) { return $blogs; } if ( $_SERVER['HTTP_HOST'] === $domain && $_SERVER['REQUEST_URI'] === $path ) { return $blogs; } else { foreach ( (array) $blogs as $blog ) { if ( str_contains( $blog['url'], $_SERVER['HTTP_HOST'] ) ) { return array( $blog ); } } return array(); } } /** * Retrieves user's data. * * Gives your client some info about you, so you don't have to. * * @since 1.5.0 * * @param array $args { * Method arguments. Note: arguments must be ordered as documented. * * @type int $0 Blog ID (unused). * @type string $1 Username. * @type string $2 Password. * } * @return array|IXR_Error */ public function blogger_getUserInfo( $args ) { $this->escape( $args ); $username = $args[1]; $password = $args[2]; $user = $this->login( $username, $password ); if ( ! $user ) { return $this->error; } if ( ! current_user_can( 'edit_posts' ) ) { return new IXR_Error( 401, __( 'Sorry, you are not allowed to access user data on this site.' ) ); } /** This action is documented in wp-includes/class-wp-xmlrpc-server.php */ do_action( 'xmlrpc_call', 'blogger.getUserInfo', $args, $this ); $struct = array( 'nickname' => $user->nickname, 'userid' => $user->ID, 'url' => $user->user_url, 'lastname' => $user->last_name, 'firstname' => $user->first_name, ); return $struct; } /** * Retrieves a post. * * @since 1.5.0 * * @param array $args { * Method arguments. Note: arguments must be ordered as documented. * * @type int $0 Blog ID (unused). * @type int $1 Post ID. * @type string $2 Username. * @type string $3 Password. * } * @return array|IXR_Error */ public function blogger_getPost( $args ) { $this->escape( $args ); $post_id = (int) $args[1]; $username = $args[2]; $password = $args[3]; $user = $this->login( $username, $password ); if ( ! $user ) { return $this->error; } $post_data = get_post( $post_id, ARRAY_A ); if ( ! $post_data ) { return new IXR_Error( 404, __( 'Invalid post ID.' ) ); } if ( ! current_user_can( 'edit_post', $post_id ) ) { return new IXR_Error( 401, __( 'Sorry, you are not allowed to edit this post.' ) ); } /** This action is documented in wp-includes/class-wp-xmlrpc-server.php */ do_action( 'xmlrpc_call', 'blogger.getPost', $args, $this ); $categories = implode( ',', wp_get_post_categories( $post_id ) ); $content = '
document_title_separator' ) );
}
/**
* Filters the blog title for display of the feed title.
*
* @since 2.2.0
* @since 4.4.0 The `$sep` parameter was deprecated and renamed to `$deprecated`.
*
* @see get_wp_title_rss()
*
* @param string $wp_title_rss The current blog title.
* @param string $deprecated Unused.
*/
echo apply_filters( 'wp_title_rss', get_wp_title_rss(), $deprecated );
}
/**
* Retrieves the current post title for the feed.
*
* @since 2.0.0
* @since 6.6.0 Added the `$post` parameter.
*
* @param int|WP_Post $post Optional. Post ID or WP_Post object. Default is global $post.
* @return string Current post title.
*/
function get_the_title_rss( $post = 0 ) {
$title = get_the_title( $post );
/**
* Filters the post title for use in a feed.
*
* @since 1.2.0
*
* @param string $title The current post title.
*/
return apply_filters( 'the_title_rss', $title );
}
/**
* Displays the post title in the feed.
*
* @since 0.71
*/
function the_title_rss() {
echo get_the_title_rss();
}
/**
* Retrieves the post content for feeds.
*
* @since 2.9.0
*
* @see get_the_content()
*
* @param string $feed_type The type of feed. rss2 | atom | rss | rdf
* @return string The filtered content.
*/
function get_the_content_feed( $feed_type = null ) {
if ( ! $feed_type ) {
$feed_type = get_default_feed();
}
/** This filter is documented in wp-includes/post-template.php */
$content = apply_filters( 'the_content', get_the_content() );
$content = str_replace( ']]>', ']]>', $content );
/**
* Filters the post content for use in feeds.
*
* @since 2.9.0
*
* @param string $content The current post content.
* @param string $feed_type Type of feed. Possible values include 'rss2', 'atom'.
* Default 'rss2'.
*/
return apply_filters( 'the_content_feed', $content, $feed_type );
}
/**
* Displays the post content for feeds.
*
* @since 2.9.0
*
* @param string $feed_type The type of feed. rss2 | atom | rss | rdf
*/
function the_content_feed( $feed_type = null ) {
echo get_the_content_feed( $feed_type );
}
/**
* Displays the post excerpt for the feed.
*
* @since 0.71
*/
function the_excerpt_rss() {
$output = get_the_excerpt();
/**
* Filters the post excerpt for a feed.
*
* @since 1.2.0
*
* @param string $output The current post excerpt.
*/
echo apply_filters( 'the_excerpt_rss', $output );
}
/**
* Displays the permalink to the post for use in feeds.
*
* @since 2.3.0
*/
function the_permalink_rss() {
/**
* Filters the permalink to the post for use in feeds.
*
* @since 2.3.0
*
* @param string $post_permalink The current post permalink.
*/
echo esc_url( apply_filters( 'the_permalink_rss', get_permalink() ) );
}
/**
* Outputs the link to the comments for the current post in an XML safe way.
*
* @since 3.0.0
*/
function comments_link_feed() {
/**
* Filters the comments permalink for the current post.
*
* @since 3.6.0
*
* @param string $comment_permalink The current comment permalink with
* '#comments' appended.
*/
echo esc_url( apply_filters( 'comments_link_feed', get_comments_link() ) );
}
/**
* Displays the feed GUID for the current comment.
*
* @since 2.5.0
*
* @param int|WP_Comment $comment_id Optional comment object or ID. Defaults to global comment object.
*/
function comment_guid( $comment_id = null ) {
echo esc_url( get_comment_guid( $comment_id ) );
}
/**
* Retrieves the feed GUID for the current comment.
*
* @since 2.5.0
*
* @param int|WP_Comment $comment_id Optional comment object or ID. Defaults to global comment object.
* @return string|false GUID for comment on success, false on failure.
*/
function get_comment_guid( $comment_id = null ) {
$comment = get_comment( $comment_id );
if ( ! is_object( $comment ) ) {
return false;
}
return get_the_guid( $comment->comment_post_ID ) . '#comment-' . $comment->comment_ID;
}
/**
* Displays the link to the comments.
*
* @since 1.5.0
* @since 4.4.0 Introduced the `$comment` argument.
*
* @param int|WP_Comment $comment Optional. Comment object or ID. Defaults to global comment object.
*/
function comment_link( $comment = null ) {
/**
* Filters the current comment's permalink.
*
* @since 3.6.0
*
* @see get_comment_link()
*
* @param string $comment_permalink The current comment permalink.
*/
echo esc_url( apply_filters( 'comment_link', get_comment_link( $comment ) ) );
}
/**
* Retrieves the current comment author for use in the feeds.
*
* @since 2.0.0
*
* @return string Comment Author.
*/
function get_comment_author_rss() {
/**
* Filters the current comment author for use in a feed.
*
* @since 1.5.0
*
* @see get_comment_author()
*
* @param string $comment_author The current comment author.
*/
return apply_filters( 'comment_author_rss', get_comment_author() );
}
/**
* Displays the current comment author in the feed.
*
* @since 1.0.0
*/
function comment_author_rss() {
echo get_comment_author_rss();
}
/**
* Displays the current comment content for use in the feeds.
*
* @since 1.0.0
*/
function comment_text_rss() {
$comment_text = get_comment_text();
/**
* Filters the current comment content for use in a feed.
*
* @since 1.5.0
*
* @param string $comment_text The content of the current comment.
*/
$comment_text = apply_filters( 'comment_text_rss', $comment_text );
echo $comment_text;
}
/**
* Retrieves all of the post categories, formatted for use in feeds.
*
* All of the categories for the current post in the feed loop, will be
* retrieved and have feed markup added, so that they can easily be added to the
* RSS2, Atom, or RSS1 and RSS0.91 RDF feeds.
*
* @since 2.1.0
*
* @param string $type Optional, default is the type returned by get_default_feed().
* @return string All of the post categories for displaying in the feed.
*/
function get_the_category_rss( $type = null ) {
if ( empty( $type ) ) {
$type = get_default_feed();
}
$categories = get_the_category();
$tags = get_the_tags();
$the_list = '';
$cat_names = array();
$filter = 'rss';
if ( 'atom' === $type ) {
$filter = 'raw';
}
if ( ! empty( $categories ) ) {
foreach ( (array) $categories as $category ) {
$cat_names[] = sanitize_term_field( 'name', $category->name, $category->term_id, 'category', $filter );
}
}
if ( ! empty( $tags ) ) {
foreach ( (array) $tags as $tag ) {
$cat_names[] = sanitize_term_field( 'name', $tag->name, $tag->term_id, 'post_tag', $filter );
}
}
$cat_names = array_unique( $cat_names );
foreach ( $cat_names as $cat_name ) {
if ( 'rdf' === $type ) {
$the_list .= "\t\tadmin_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()
* @since 2.1.0
*
* @global WP_Scripts $wp_scripts The WP_Scripts object for printing scripts.
*
* @param string|string[]|false $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.
* @since 6.3.0 The $in_footer parameter of type boolean was overloaded to be an $args parameter of type array.
* @since 6.9.0 The $fetchpriority parameter of type string was added to the $args parameter of type array.
*
* @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 array|bool $args {
* Optional. An array of additional script loading strategies. Default empty array.
* Otherwise, it may be a boolean in which case it determines whether the script is printed in the footer. Default false.
*
* @type string $strategy Optional. If provided, may be either 'defer' or 'async'.
* @type bool $in_footer Optional. Whether to print the script in the footer. Default 'false'.
* @type string $fetchpriority Optional. The fetch priority for the script. Default 'auto'.
* }
* @return bool Whether the script has been registered. True on success, false on failure.
*/
function wp_register_script( $handle, $src, $deps = array(), $ver = false, $args = array() ) {
if ( ! is_array( $args ) ) {
$args = array(
'in_footer' => (bool) $args,
);
}
_wp_scripts_maybe_doing_it_wrong( __FUNCTION__, $handle );
$wp_scripts = wp_scripts();
$registered = $wp_scripts->add( $handle, $src, $deps, $ver );
if ( ! empty( $args['in_footer'] ) ) {
$wp_scripts->add_data( $handle, 'group', 1 );
}
if ( ! empty( $args['strategy'] ) ) {
$wp_scripts->add_data( $handle, 'strategy', $args['strategy'] );
}
if ( ! empty( $args['fetchpriority'] ) ) {
$wp_scripts->add_data( $handle, 'fetchpriority', $args['fetchpriority'] );
}
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
*
* @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 ) {
$wp_scripts = wp_scripts();
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()
* @since 5.0.0
* @since 5.1.0 The `$domain` parameter was made optional.
*
* @global WP_Scripts $wp_scripts The WP_Scripts object for printing scripts.
*
* @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
* @since 6.3.0 The $in_footer parameter of type boolean was overloaded to be an $args parameter of type array.
* @since 6.9.0 The $fetchpriority parameter of type string was added to the $args parameter of type array.
*
* @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 array|bool $args {
* Optional. An array of additional script loading strategies. Default empty array.
* Otherwise, it may be a boolean in which case it determines whether the script is printed in the footer. Default false.
*
* @type string $strategy Optional. If provided, may be either 'defer' or 'async'.
* @type bool $in_footer Optional. Whether to print the script in the footer. Default 'false'.
* @type string $fetchpriority Optional. The fetch priority for the script. Default 'auto'.
* }
*/
function wp_enqueue_script( $handle, $src = '', $deps = array(), $ver = false, $args = array() ) {
_wp_scripts_maybe_doing_it_wrong( __FUNCTION__, $handle );
$wp_scripts = wp_scripts();
if ( $src || ! empty( $args ) ) {
$_handle = explode( '?', $handle );
if ( ! is_array( $args ) ) {
$args = array(
'in_footer' => (bool) $args,
);
}
if ( $src ) {
$wp_scripts->add( $_handle[0], $src, $deps, $ver );
}
if ( ! empty( $args['in_footer'] ) ) {
$wp_scripts->add_data( $_handle[0], 'group', 1 );
}
if ( ! empty( $args['strategy'] ) ) {
$wp_scripts->add_data( $_handle[0], 'strategy', $args['strategy'] );
}
if ( ! empty( $args['fetchpriority'] ) ) {
$wp_scripts->add_data( $_handle[0], 'fetchpriority', $args['fetchpriority'] );
}
}
$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:
* 'strategy' string 'defer' or 'async'.
*
* @since 4.2.0
* @since 6.9.0 Updated possible values to remove reference to 'conditional' and add 'strategy'.
*
* @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 );
}
* @author Matt Knapp
* // create a new instance of Services_JSON
* $json = new Services_JSON();
*
* // convert a complex value to JSON notation, and send it to the browser
* $value = array('foo', 'bar', array(1, 2, 'baz'), array(3, array(4)));
* $output = $json->encode($value);
*
* print($output);
* // prints: ["foo","bar",[1,2,"baz"],[3,[4]]]
*
* // accept incoming POST data, assumed to be in JSON notation
* $input = file_get_contents('php://input', 1000000);
* $value = $json->decode($input);
*
*/
class Services_JSON
{
/**
* Object behavior flags.
*
* @var int
*/
public $use;
// private - cache the mbstring lookup results..
var $_mb_strlen = false;
var $_mb_substr = false;
var $_mb_convert_encoding = false;
/**
* constructs a new JSON instance
*
* @deprecated 5.3.0 Use the PHP native JSON extension instead.
*
* @param int $use object behavior flags; combine with boolean-OR
*
* possible values:
* - SERVICES_JSON_LOOSE_TYPE: loose typing.
* "{...}" syntax creates associative arrays
* instead of objects in decode().
* - SERVICES_JSON_SUPPRESS_ERRORS: error suppression.
* Values which can't be encoded (e.g. resources)
* appear as NULL instead of throwing errors.
* By default, a deeply-nested resource will
* bubble up with an error, so all return values
* from encode() should be checked with isError()
* - SERVICES_JSON_USE_TO_JSON: call toJSON when serializing objects
* It serializes the return value from the toJSON call rather
* than the object itself, toJSON can return associative arrays,
* strings or numbers, if you return an object, make sure it does
* not have a toJSON method, otherwise an error will occur.
*/
function __construct( $use = 0 )
{
_deprecated_function( __METHOD__, '5.3.0', 'The PHP native JSON extension' );
$this->use = $use;
$this->_mb_strlen = function_exists('mb_strlen');
$this->_mb_convert_encoding = function_exists('mb_convert_encoding');
$this->_mb_substr = function_exists('mb_substr');
}
/**
* PHP4 constructor.
*
* @deprecated 5.3.0 Use __construct() instead.
*
* @see Services_JSON::__construct()
*/
public function Services_JSON( $use = 0 ) {
_deprecated_constructor( 'Services_JSON', '5.3.0', get_class( $this ) );
self::__construct( $use );
}
/**
* convert a string from one UTF-16 char to one UTF-8 char
*
* Normally should be handled by mb_convert_encoding, but
* provides a slower PHP-only method for installations
* that lack the multibye string extension.
*
* @deprecated 5.3.0 Use the PHP native JSON extension instead.
*
* @param string $utf16 UTF-16 character
* @return string UTF-8 character
* @access private
*/
function utf162utf8($utf16)
{
_deprecated_function( __METHOD__, '5.3.0', 'The PHP native JSON extension' );
// oh please oh please oh please oh please oh please
if($this->_mb_convert_encoding) {
return mb_convert_encoding($utf16, 'UTF-8', 'UTF-16');
}
$bytes = (ord($utf16[0]) << 8) | ord($utf16[1]);
switch(true) {
case ((0x7F & $bytes) == $bytes):
// this case should never be reached, because we are in ASCII range
// see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
return chr(0x7F & $bytes);
case (0x07FF & $bytes) == $bytes:
// return a 2-byte UTF-8 character
// see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
return chr(0xC0 | (($bytes >> 6) & 0x1F))
. chr(0x80 | ($bytes & 0x3F));
case (0xFFFF & $bytes) == $bytes:
// return a 3-byte UTF-8 character
// see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
return chr(0xE0 | (($bytes >> 12) & 0x0F))
. chr(0x80 | (($bytes >> 6) & 0x3F))
. chr(0x80 | ($bytes & 0x3F));
}
// ignoring UTF-32 for now, sorry
return '';
}
/**
* convert a string from one UTF-8 char to one UTF-16 char
*
* Normally should be handled by mb_convert_encoding, but
* provides a slower PHP-only method for installations
* that lack the multibyte string extension.
*
* @deprecated 5.3.0 Use the PHP native JSON extension instead.
*
* @param string $utf8 UTF-8 character
* @return string UTF-16 character
* @access private
*/
function utf82utf16($utf8)
{
_deprecated_function( __METHOD__, '5.3.0', 'The PHP native JSON extension' );
// oh please oh please oh please oh please oh please
if($this->_mb_convert_encoding) {
return mb_convert_encoding($utf8, 'UTF-16', 'UTF-8');
}
switch($this->strlen8($utf8)) {
case 1:
// this case should never be reached, because we are in ASCII range
// see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
return $utf8;
case 2:
// return a UTF-16 character from a 2-byte UTF-8 char
// see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
return chr(0x07 & (ord($utf8[0]) >> 2))
. chr((0xC0 & (ord($utf8[0]) << 6))
| (0x3F & ord($utf8[1])));
case 3:
// return a UTF-16 character from a 3-byte UTF-8 char
// see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
return chr((0xF0 & (ord($utf8[0]) << 4))
| (0x0F & (ord($utf8[1]) >> 2)))
. chr((0xC0 & (ord($utf8[1]) << 6))
| (0x7F & ord($utf8[2])));
}
// ignoring UTF-32 for now, sorry
return '';
}
/**
* encodes an arbitrary variable into JSON format (and sends JSON Header)
*
* @deprecated 5.3.0 Use the PHP native JSON extension instead.
*
* @param mixed $var any number, boolean, string, array, or object to be encoded.
* see argument 1 to Services_JSON() above for array-parsing behavior.
* if var is a string, note that encode() always expects it
* to be in ASCII or UTF-8 format!
*
* @return mixed JSON string representation of input var or an error if a problem occurs
* @access public
*/
function encode($var)
{
_deprecated_function( __METHOD__, '5.3.0', 'The PHP native JSON extension' );
header('Content-Type: application/json');
return $this->encodeUnsafe($var);
}
/**
* encodes an arbitrary variable into JSON format without JSON Header - warning - may allow XSS!!!!)
*
* @deprecated 5.3.0 Use the PHP native JSON extension instead.
*
* @param mixed $var any number, boolean, string, array, or object to be encoded.
* see argument 1 to Services_JSON() above for array-parsing behavior.
* if var is a string, note that encode() always expects it
* to be in ASCII or UTF-8 format!
*
* @return mixed JSON string representation of input var or an error if a problem occurs
* @access public
*/
function encodeUnsafe($var)
{
_deprecated_function( __METHOD__, '5.3.0', 'The PHP native JSON extension' );
// see bug #16908 - regarding numeric locale printing
$lc = setlocale(LC_NUMERIC, 0);
setlocale(LC_NUMERIC, 'C');
$ret = $this->_encode($var);
setlocale(LC_NUMERIC, $lc);
return $ret;
}
/**
* PRIVATE CODE that does the work of encodes an arbitrary variable into JSON format
*
* @deprecated 5.3.0 Use the PHP native JSON extension instead.
*
* @param mixed $var any number, boolean, string, array, or object to be encoded.
* see argument 1 to Services_JSON() above for array-parsing behavior.
* if var is a string, note that encode() always expects it
* to be in ASCII or UTF-8 format!
*
* @return mixed JSON string representation of input var or an error if a problem occurs
* @access public
*/
function _encode($var)
{
_deprecated_function( __METHOD__, '5.3.0', 'The PHP native JSON extension' );
switch (gettype($var)) {
case 'boolean':
return $var ? 'true' : 'false';
case 'NULL':
return 'null';
case 'integer':
return (int) $var;
case 'double':
case 'float':
return (float) $var;
case 'string':
// STRINGS ARE EXPECTED TO BE IN ASCII OR UTF-8 FORMAT
$ascii = '';
$strlen_var = $this->strlen8($var);
/*
* Iterate over every character in the string,
* escaping with a slash or encoding to UTF-8 where necessary
*/
for ($c = 0; $c < $strlen_var; ++$c) {
$ord_var_c = ord($var[$c]);
switch (true) {
case $ord_var_c == 0x08:
$ascii .= '\b';
break;
case $ord_var_c == 0x09:
$ascii .= '\t';
break;
case $ord_var_c == 0x0A:
$ascii .= '\n';
break;
case $ord_var_c == 0x0C:
$ascii .= '\f';
break;
case $ord_var_c == 0x0D:
$ascii .= '\r';
break;
case $ord_var_c == 0x22:
case $ord_var_c == 0x2F:
case $ord_var_c == 0x5C:
// double quote, slash, slosh
$ascii .= '\\'.$var[$c];
break;
case (($ord_var_c >= 0x20) && ($ord_var_c <= 0x7F)):
// characters U-00000000 - U-0000007F (same as ASCII)
$ascii .= $var[$c];
break;
case (($ord_var_c & 0xE0) == 0xC0):
// characters U-00000080 - U-000007FF, mask 110XXXXX
// see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
if ($c+1 >= $strlen_var) {
$c += 1;
$ascii .= '?';
break;
}
$char = pack('C*', $ord_var_c, ord($var[$c + 1]));
$c += 1;
$utf16 = $this->utf82utf16($char);
$ascii .= sprintf('\u%04s', bin2hex($utf16));
break;
case (($ord_var_c & 0xF0) == 0xE0):
if ($c+2 >= $strlen_var) {
$c += 2;
$ascii .= '?';
break;
}
// characters U-00000800 - U-0000FFFF, mask 1110XXXX
// see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
$char = pack('C*', $ord_var_c,
@ord($var[$c + 1]),
@ord($var[$c + 2]));
$c += 2;
$utf16 = $this->utf82utf16($char);
$ascii .= sprintf('\u%04s', bin2hex($utf16));
break;
case (($ord_var_c & 0xF8) == 0xF0):
if ($c+3 >= $strlen_var) {
$c += 3;
$ascii .= '?';
break;
}
// characters U-00010000 - U-001FFFFF, mask 11110XXX
// see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
$char = pack('C*', $ord_var_c,
ord($var[$c + 1]),
ord($var[$c + 2]),
ord($var[$c + 3]));
$c += 3;
$utf16 = $this->utf82utf16($char);
$ascii .= sprintf('\u%04s', bin2hex($utf16));
break;
case (($ord_var_c & 0xFC) == 0xF8):
// characters U-00200000 - U-03FFFFFF, mask 111110XX
// see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
if ($c+4 >= $strlen_var) {
$c += 4;
$ascii .= '?';
break;
}
$char = pack('C*', $ord_var_c,
ord($var[$c + 1]),
ord($var[$c + 2]),
ord($var[$c + 3]),
ord($var[$c + 4]));
$c += 4;
$utf16 = $this->utf82utf16($char);
$ascii .= sprintf('\u%04s', bin2hex($utf16));
break;
case (($ord_var_c & 0xFE) == 0xFC):
if ($c+5 >= $strlen_var) {
$c += 5;
$ascii .= '?';
break;
}
// characters U-04000000 - U-7FFFFFFF, mask 1111110X
// see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
$char = pack('C*', $ord_var_c,
ord($var[$c + 1]),
ord($var[$c + 2]),
ord($var[$c + 3]),
ord($var[$c + 4]),
ord($var[$c + 5]));
$c += 5;
$utf16 = $this->utf82utf16($char);
$ascii .= sprintf('\u%04s', bin2hex($utf16));
break;
}
}
return '"'.$ascii.'"';
case 'array':
/*
* As per JSON spec if any array key is not an integer
* we must treat the whole array as an object. We
* also try to catch a sparsely populated associative
* array with numeric keys here because some JS engines
* will create an array with empty indexes up to
* max_index which can cause memory issues and because
* the keys, which may be relevant, will be remapped
* otherwise.
*
* As per the ECMA and JSON specification an object may
* have any string as a property. Unfortunately due to
* a hole in the ECMA specification if the key is a
* ECMA reserved word or starts with a digit the
* parameter is only accessible using ECMAScript's
* bracket notation.
*/
// treat as a JSON object
if (is_array($var) && count($var) && (array_keys($var) !== range(0, sizeof($var) - 1))) {
$properties = array_map(array($this, 'name_value'),
array_keys($var),
array_values($var));
foreach($properties as $property) {
if(Services_JSON::isError($property)) {
return $property;
}
}
return '{' . join(',', $properties) . '}';
}
// treat it like a regular array
$elements = array_map(array($this, '_encode'), $var);
foreach($elements as $element) {
if(Services_JSON::isError($element)) {
return $element;
}
}
return '[' . join(',', $elements) . ']';
case 'object':
// support toJSON methods.
if (($this->use & SERVICES_JSON_USE_TO_JSON) && method_exists($var, 'toJSON')) {
// this may end up allowing unlimited recursion
// so we check the return value to make sure it's not got the same method.
$recode = $var->toJSON();
if (method_exists($recode, 'toJSON')) {
return ($this->use & SERVICES_JSON_SUPPRESS_ERRORS)
? 'null'
: new Services_JSON_Error(get_class($var).
" toJSON returned an object with a toJSON method.");
}
return $this->_encode( $recode );
}
$vars = get_object_vars($var);
$properties = array_map(array($this, 'name_value'),
array_keys($vars),
array_values($vars));
foreach($properties as $property) {
if(Services_JSON::isError($property)) {
return $property;
}
}
return '{' . join(',', $properties) . '}';
default:
return ($this->use & SERVICES_JSON_SUPPRESS_ERRORS)
? 'null'
: new Services_JSON_Error(gettype($var)." can not be encoded as JSON string");
}
}
/**
* array-walking function for use in generating JSON-formatted name-value pairs
*
* @deprecated 5.3.0 Use the PHP native JSON extension instead.
*
* @param string $name name of key to use
* @param mixed $value reference to an array element to be encoded
*
* @return string JSON-formatted name-value pair, like '"name":value'
* @access private
*/
function name_value($name, $value)
{
_deprecated_function( __METHOD__, '5.3.0', 'The PHP native JSON extension' );
$encoded_value = $this->_encode($value);
if(Services_JSON::isError($encoded_value)) {
return $encoded_value;
}
return $this->_encode((string) $name) . ':' . $encoded_value;
}
/**
* reduce a string by removing leading and trailing comments and whitespace
*
* @deprecated 5.3.0 Use the PHP native JSON extension instead.
*
* @param $str string string value to strip of comments and whitespace
*
* @return string string value stripped of comments and whitespace
* @access private
*/
function reduce_string($str)
{
_deprecated_function( __METHOD__, '5.3.0', 'The PHP native JSON extension' );
$str = preg_replace(array(
// eliminate single line comments in '// ...' form
'#^\s*//(.+)$#m',
// eliminate multi-line comments in '/* ... */' form, at start of string
'#^\s*/\*(.+)\*/#Us',
// eliminate multi-line comments in '/* ... */' form, at end of string
'#/\*(.+)\*/\s*$#Us'
), '', $str);
// eliminate extraneous space
return trim($str);
}
/**
* decodes a JSON string into appropriate variable
*
* @deprecated 5.3.0 Use the PHP native JSON extension instead.
*
* @param string $str JSON-formatted string
*
* @return mixed number, boolean, string, array, or object
* corresponding to given JSON input string.
* See argument 1 to Services_JSON() above for object-output behavior.
* Note that decode() always returns strings
* in ASCII or UTF-8 format!
* @access public
*/
function decode($str)
{
_deprecated_function( __METHOD__, '5.3.0', 'The PHP native JSON extension' );
$str = $this->reduce_string($str);
switch (strtolower($str)) {
case 'true':
return true;
case 'false':
return false;
case 'null':
return null;
default:
$m = array();
if (is_numeric($str)) {
// Lookie-loo, it's a number
// This would work on its own, but I'm trying to be
// good about returning integers where appropriate:
// return (float)$str;
// Return float or int, as appropriate
return ((float)$str == (int)$str)
? (int)$str
: (float)$str;
} elseif (preg_match('/^("|\').*(\1)$/s', $str, $m) && $m[1] == $m[2]) {
// STRINGS RETURNED IN UTF-8 FORMAT
$delim = $this->substr8($str, 0, 1);
$chrs = $this->substr8($str, 1, -1);
$utf8 = '';
$strlen_chrs = $this->strlen8($chrs);
for ($c = 0; $c < $strlen_chrs; ++$c) {
$substr_chrs_c_2 = $this->substr8($chrs, $c, 2);
$ord_chrs_c = ord($chrs[$c]);
switch (true) {
case $substr_chrs_c_2 == '\b':
$utf8 .= chr(0x08);
++$c;
break;
case $substr_chrs_c_2 == '\t':
$utf8 .= chr(0x09);
++$c;
break;
case $substr_chrs_c_2 == '\n':
$utf8 .= chr(0x0A);
++$c;
break;
case $substr_chrs_c_2 == '\f':
$utf8 .= chr(0x0C);
++$c;
break;
case $substr_chrs_c_2 == '\r':
$utf8 .= chr(0x0D);
++$c;
break;
case $substr_chrs_c_2 == '\\"':
case $substr_chrs_c_2 == '\\\'':
case $substr_chrs_c_2 == '\\\\':
case $substr_chrs_c_2 == '\\/':
if (($delim == '"' && $substr_chrs_c_2 != '\\\'') ||
($delim == "'" && $substr_chrs_c_2 != '\\"')) {
$utf8 .= $chrs[++$c];
}
break;
case preg_match('/\\\u[0-9A-F]{4}/i', $this->substr8($chrs, $c, 6)):
// single, escaped unicode character
$utf16 = chr(hexdec($this->substr8($chrs, ($c + 2), 2)))
. chr(hexdec($this->substr8($chrs, ($c + 4), 2)));
$utf8 .= $this->utf162utf8($utf16);
$c += 5;
break;
case ($ord_chrs_c >= 0x20) && ($ord_chrs_c <= 0x7F):
$utf8 .= $chrs[$c];
break;
case ($ord_chrs_c & 0xE0) == 0xC0:
// characters U-00000080 - U-000007FF, mask 110XXXXX
//see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
$utf8 .= $this->substr8($chrs, $c, 2);
++$c;
break;
case ($ord_chrs_c & 0xF0) == 0xE0:
// characters U-00000800 - U-0000FFFF, mask 1110XXXX
// see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
$utf8 .= $this->substr8($chrs, $c, 3);
$c += 2;
break;
case ($ord_chrs_c & 0xF8) == 0xF0:
// characters U-00010000 - U-001FFFFF, mask 11110XXX
// see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
$utf8 .= $this->substr8($chrs, $c, 4);
$c += 3;
break;
case ($ord_chrs_c & 0xFC) == 0xF8:
// characters U-00200000 - U-03FFFFFF, mask 111110XX
// see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
$utf8 .= $this->substr8($chrs, $c, 5);
$c += 4;
break;
case ($ord_chrs_c & 0xFE) == 0xFC:
// characters U-04000000 - U-7FFFFFFF, mask 1111110X
// see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
$utf8 .= $this->substr8($chrs, $c, 6);
$c += 5;
break;
}
}
return $utf8;
} elseif (preg_match('/^\[.*\]$/s', $str) || preg_match('/^\{.*\}$/s', $str)) {
// array, or object notation
if ($str[0] == '[') {
$stk = array(SERVICES_JSON_IN_ARR);
$arr = array();
} else {
if ($this->use & SERVICES_JSON_LOOSE_TYPE) {
$stk = array(SERVICES_JSON_IN_OBJ);
$obj = array();
} else {
$stk = array(SERVICES_JSON_IN_OBJ);
$obj = new stdClass();
}
}
array_push($stk, array('what' => SERVICES_JSON_SLICE,
'where' => 0,
'delim' => false));
$chrs = $this->substr8($str, 1, -1);
$chrs = $this->reduce_string($chrs);
if ($chrs == '') {
if (reset($stk) == SERVICES_JSON_IN_ARR) {
return $arr;
} else {
return $obj;
}
}
//print("\nparsing {$chrs}\n");
$strlen_chrs = $this->strlen8($chrs);
for ($c = 0; $c <= $strlen_chrs; ++$c) {
$top = end($stk);
$substr_chrs_c_2 = $this->substr8($chrs, $c, 2);
if (($c == $strlen_chrs) || (($chrs[$c] == ',') && ($top['what'] == SERVICES_JSON_SLICE))) {
// found a comma that is not inside a string, array, etc.,
// OR we've reached the end of the character list
$slice = $this->substr8($chrs, $top['where'], ($c - $top['where']));
array_push($stk, array('what' => SERVICES_JSON_SLICE, 'where' => ($c + 1), 'delim' => false));
//print("Found split at {$c}: ".$this->substr8($chrs, $top['where'], (1 + $c - $top['where']))."\n");
if (reset($stk) == SERVICES_JSON_IN_ARR) {
// we are in an array, so just push an element onto the stack
array_push($arr, $this->decode($slice));
} elseif (reset($stk) == SERVICES_JSON_IN_OBJ) {
// we are in an object, so figure
// out the property name and set an
// element in an associative array,
// for now
$parts = array();
if (preg_match('/^\s*(["\'].*[^\\\]["\'])\s*:/Uis', $slice, $parts)) {
// "name":value pair
$key = $this->decode($parts[1]);
$val = $this->decode(trim(substr($slice, strlen($parts[0])), ", \t\n\r\0\x0B"));
if ($this->use & SERVICES_JSON_LOOSE_TYPE) {
$obj[$key] = $val;
} else {
$obj->$key = $val;
}
} elseif (preg_match('/^\s*(\w+)\s*:/Uis', $slice, $parts)) {
// name:value pair, where name is unquoted
$key = $parts[1];
$val = $this->decode(trim(substr($slice, strlen($parts[0])), ", \t\n\r\0\x0B"));
if ($this->use & SERVICES_JSON_LOOSE_TYPE) {
$obj[$key] = $val;
} else {
$obj->$key = $val;
}
}
}
} elseif ((($chrs[$c] == '"') || ($chrs[$c] == "'")) && ($top['what'] != SERVICES_JSON_IN_STR)) {
// found a quote, and we are not inside a string
array_push($stk, array('what' => SERVICES_JSON_IN_STR, 'where' => $c, 'delim' => $chrs[$c]));
//print("Found start of string at {$c}\n");
} elseif (($chrs[$c] == $top['delim']) &&
($top['what'] == SERVICES_JSON_IN_STR) &&
(($this->strlen8($this->substr8($chrs, 0, $c)) - $this->strlen8(rtrim($this->substr8($chrs, 0, $c), '\\'))) % 2 != 1)) {
// found a quote, we're in a string, and it's not escaped
// we know that it's not escaped because there is _not_ an
// odd number of backslashes at the end of the string so far
array_pop($stk);
//print("Found end of string at {$c}: ".$this->substr8($chrs, $top['where'], (1 + 1 + $c - $top['where']))."\n");
} elseif (($chrs[$c] == '[') &&
in_array($top['what'], array(SERVICES_JSON_SLICE, SERVICES_JSON_IN_ARR, SERVICES_JSON_IN_OBJ))) {
// found a left-bracket, and we are in an array, object, or slice
array_push($stk, array('what' => SERVICES_JSON_IN_ARR, 'where' => $c, 'delim' => false));
//print("Found start of array at {$c}\n");
} elseif (($chrs[$c] == ']') && ($top['what'] == SERVICES_JSON_IN_ARR)) {
// found a right-bracket, and we're in an array
array_pop($stk);
//print("Found end of array at {$c}: ".$this->substr8($chrs, $top['where'], (1 + $c - $top['where']))."\n");
} elseif (($chrs[$c] == '{') &&
in_array($top['what'], array(SERVICES_JSON_SLICE, SERVICES_JSON_IN_ARR, SERVICES_JSON_IN_OBJ))) {
// found a left-brace, and we are in an array, object, or slice
array_push($stk, array('what' => SERVICES_JSON_IN_OBJ, 'where' => $c, 'delim' => false));
//print("Found start of object at {$c}\n");
} elseif (($chrs[$c] == '}') && ($top['what'] == SERVICES_JSON_IN_OBJ)) {
// found a right-brace, and we're in an object
array_pop($stk);
//print("Found end of object at {$c}: ".$this->substr8($chrs, $top['where'], (1 + $c - $top['where']))."\n");
} elseif (($substr_chrs_c_2 == '/*') &&
in_array($top['what'], array(SERVICES_JSON_SLICE, SERVICES_JSON_IN_ARR, SERVICES_JSON_IN_OBJ))) {
// found a comment start, and we are in an array, object, or slice
array_push($stk, array('what' => SERVICES_JSON_IN_CMT, 'where' => $c, 'delim' => false));
$c++;
//print("Found start of comment at {$c}\n");
} elseif (($substr_chrs_c_2 == '*/') && ($top['what'] == SERVICES_JSON_IN_CMT)) {
// found a comment end, and we're in one now
array_pop($stk);
$c++;
for ($i = $top['where']; $i <= $c; ++$i)
$chrs = substr_replace($chrs, ' ', $i, 1);
//print("Found end of comment at {$c}: ".$this->substr8($chrs, $top['where'], (1 + $c - $top['where']))."\n");
}
}
if (reset($stk) == SERVICES_JSON_IN_ARR) {
return $arr;
} elseif (reset($stk) == SERVICES_JSON_IN_OBJ) {
return $obj;
}
}
}
}
/**
* @deprecated 5.3.0 Use the PHP native JSON extension instead.
*
* @todo Ultimately, this should just call PEAR::isError()
*/
function isError($data, $code = null)
{
_deprecated_function( __METHOD__, '5.3.0', 'The PHP native JSON extension' );
if (class_exists('pear')) {
return PEAR::isError($data, $code);
} elseif (is_object($data) && ($data instanceof services_json_error ||
is_subclass_of($data, 'services_json_error'))) {
return true;
}
return false;
}
/**
* Calculates length of string in bytes
*
* @deprecated 5.3.0 Use the PHP native JSON extension instead.
*
* @param string
* @return integer length
*/
function strlen8( $str )
{
_deprecated_function( __METHOD__, '5.3.0', 'The PHP native JSON extension' );
if ( $this->_mb_strlen ) {
return mb_strlen( $str, "8bit" );
}
return strlen( $str );
}
/**
* Returns part of a string, interpreting $start and $length as number of bytes.
*
* @deprecated 5.3.0 Use the PHP native JSON extension instead.
*
* @param string
* @param integer start
* @param integer length
* @return integer length
*/
function substr8( $string, $start, $length=false )
{
_deprecated_function( __METHOD__, '5.3.0', 'The PHP native JSON extension' );
if ( $length === false ) {
$length = $this->strlen8( $string ) - $start;
}
if ( $this->_mb_substr ) {
return mb_substr( $string, $start, $length, "8bit" );
}
return substr( $string, $start, $length );
}
}
if (class_exists('PEAR_Error')) {
class Services_JSON_Error extends PEAR_Error
{
/**
* PHP5 constructor.
*
* @deprecated 5.3.0 Use the PHP native JSON extension instead.
*/
function __construct($message = 'unknown error', $code = null,
$mode = null, $options = null, $userinfo = null)
{
_deprecated_function( __METHOD__, '5.3.0', 'The PHP native JSON extension' );
parent::PEAR_Error($message, $code, $mode, $options, $userinfo);
}
/**
* PHP4 constructor.
*
* @deprecated 5.3.0 Use __construct() instead.
*
* @see Services_JSON_Error::__construct()
*/
public function Services_JSON_Error($message = 'unknown error', $code = null,
$mode = null, $options = null, $userinfo = null) {
_deprecated_constructor( 'Services_JSON_Error', '5.3.0', get_class( $this ) );
self::__construct($message, $code, $mode, $options, $userinfo);
}
}
} else {
/**
* @todo Ultimately, this class shall be descended from PEAR_Error
*/
class Services_JSON_Error
{
/**
* PHP5 constructor.
*
* @deprecated 5.3.0 Use the PHP native JSON extension instead.
*/
function __construct( $message = 'unknown error', $code = null,
$mode = null, $options = null, $userinfo = null )
{
_deprecated_function( __METHOD__, '5.3.0', 'The PHP native JSON extension' );
}
/**
* PHP4 constructor.
*
* @deprecated 5.3.0 Use __construct() instead.
*
* @see Services_JSON_Error::__construct()
*/
public function Services_JSON_Error( $message = 'unknown error', $code = null,
$mode = null, $options = null, $userinfo = null ) {
_deprecated_constructor( 'Services_JSON_Error', '5.3.0', get_class( $this ) );
self::__construct( $message, $code, $mode, $options, $userinfo );
}
}
}
endif;
'unique-name-within-the-set',
* 'name' => 'Name for the UI',
*
* $scope = '.a, .b .c';
* $selector = '> .x, .y';
* $merged = scope_selector( $scope, $selector );
* // $merged is '.a > .x, .a .y, .b .c > .x, .b .c .y'
*
*
* @since 5.9.0
* @since 6.6.0 Added early return if missing scope or selector.
*
* @param string $scope Selector to scope to.
* @param string $selector Original selector.
* @return string Scoped selector.
*/
public static function scope_selector( $scope, $selector ) {
if ( ! $scope || ! $selector ) {
return $selector;
}
$scopes = explode( ',', $scope );
$selectors = explode( ',', $selector );
$selectors_scoped = array();
foreach ( $scopes as $outer ) {
foreach ( $selectors as $inner ) {
$outer = trim( $outer );
$inner = trim( $inner );
if ( ! empty( $outer ) && ! empty( $inner ) ) {
$selectors_scoped[] = $outer . ' ' . $inner;
} elseif ( empty( $outer ) ) {
$selectors_scoped[] = $inner;
} elseif ( empty( $inner ) ) {
$selectors_scoped[] = $outer;
}
}
}
$result = implode( ', ', $selectors_scoped );
return $result;
}
/**
* Scopes the selectors for a given style node.
*
* This includes the primary selector, i.e. `$node['selector']`, as well as any custom
* selectors for features and subfeatures, e.g. `$node['selectors']['border']` etc.
*
* @since 6.6.0
*
* @param string $scope Selector to scope to.
* @param array $node Style node with selectors to scope.
* @return array Node with updated selectors.
*/
protected static function scope_style_node_selectors( $scope, $node ) {
$node['selector'] = static::scope_selector( $scope, $node['selector'] );
if ( empty( $node['selectors'] ) ) {
return $node;
}
foreach ( $node['selectors'] as $feature => $selector ) {
if ( is_string( $selector ) ) {
$node['selectors'][ $feature ] = static::scope_selector( $scope, $selector );
}
if ( is_array( $selector ) ) {
foreach ( $selector as $subfeature => $subfeature_selector ) {
$node['selectors'][ $feature ][ $subfeature ] = static::scope_selector( $scope, $subfeature_selector );
}
}
}
return $node;
}
/**
* Gets preset values keyed by slugs based on settings and metadata.
*
*
* $settings = array(
* 'typography' => array(
* 'fontFamilies' => array(
* array(
* 'slug' => 'sansSerif',
* 'fontFamily' => '"Helvetica Neue", sans-serif',
* ),
* array(
* 'slug' => 'serif',
* 'colors' => 'Georgia, serif',
* )
* ),
* ),
* );
* $meta = array(
* 'path' => array( 'typography', 'fontFamilies' ),
* 'value_key' => 'fontFamily',
* );
* $values_by_slug = get_settings_values_by_slug();
* // $values_by_slug === array(
* // 'sans-serif' => '"Helvetica Neue", sans-serif',
* // 'serif' => 'Georgia, serif',
* // );
*
*
* @since 5.9.0
* @since 6.6.0 Passing $settings to the callbacks defined in static::PRESETS_METADATA.
*
* @param array $settings Settings to process.
* @param array $preset_metadata One of the PRESETS_METADATA values.
* @param string[] $origins List of origins to process.
* @return array Array of presets where each key is a slug and each value is the preset value.
*/
protected static function get_settings_values_by_slug( $settings, $preset_metadata, $origins ) {
$preset_per_origin = _wp_array_get( $settings, $preset_metadata['path'], array() );
$result = array();
foreach ( $origins as $origin ) {
if ( ! isset( $preset_per_origin[ $origin ] ) ) {
continue;
}
foreach ( $preset_per_origin[ $origin ] as $preset ) {
$slug = _wp_to_kebab_case( $preset['slug'] );
$value = '';
if ( isset( $preset_metadata['value_key'], $preset[ $preset_metadata['value_key'] ] ) ) {
$value_key = $preset_metadata['value_key'];
$value = $preset[ $value_key ];
} elseif (
isset( $preset_metadata['value_func'] ) &&
is_callable( $preset_metadata['value_func'] )
) {
$value_func = $preset_metadata['value_func'];
$value = call_user_func( $value_func, $preset, $settings );
} else {
// If we don't have a value, then don't add it to the result.
continue;
}
$result[ $slug ] = $value;
}
}
return $result;
}
/**
* Similar to get_settings_values_by_slug, but doesn't compute the value.
*
* @since 5.9.0
*
* @param array $settings Settings to process.
* @param array $preset_metadata One of the PRESETS_METADATA values.
* @param string[] $origins List of origins to process.
* @return array Array of presets where the key and value are both the slug.
*/
protected static function get_settings_slugs( $settings, $preset_metadata, $origins = null ) {
if ( null === $origins ) {
$origins = static::VALID_ORIGINS;
}
$preset_per_origin = _wp_array_get( $settings, $preset_metadata['path'], array() );
$result = array();
foreach ( $origins as $origin ) {
if ( ! isset( $preset_per_origin[ $origin ] ) ) {
continue;
}
foreach ( $preset_per_origin[ $origin ] as $preset ) {
$slug = _wp_to_kebab_case( $preset['slug'] );
// Use the array as a set so we don't get duplicates.
$result[ $slug ] = $slug;
}
}
return $result;
}
/**
* Transforms a slug into a CSS Custom Property.
*
* @since 5.9.0
*
* @param string $input String to replace.
* @param string $slug The slug value to use to generate the custom property.
* @return string The CSS Custom Property. Something along the lines of `--wp--preset--color--black`.
*/
protected static function replace_slug_in_string( $input, $slug ) {
return strtr( $input, array( '$slug' => $slug ) );
}
/**
* Given the block settings, extracts the CSS Custom Properties
* for the presets and adds them to the $declarations array
* following the format:
*
* array(
* 'name' => 'property_name',
* 'value' => 'property_value,
* )
*
* @since 5.8.0
* @since 5.9.0 Added the `$origins` parameter.
*
* @param array $settings Settings to process.
* @param string[] $origins List of origins to process.
* @return array The modified $declarations.
*/
protected static function compute_preset_vars( $settings, $origins ) {
$declarations = array();
foreach ( static::PRESETS_METADATA as $preset_metadata ) {
if ( empty( $preset_metadata['css_vars'] ) ) {
continue;
}
$values_by_slug = static::get_settings_values_by_slug( $settings, $preset_metadata, $origins );
foreach ( $values_by_slug as $slug => $value ) {
$declarations[] = array(
'name' => static::replace_slug_in_string( $preset_metadata['css_vars'], $slug ),
'value' => $value,
);
}
}
return $declarations;
}
/**
* Given an array of settings, extracts the CSS Custom Properties
* for the custom values and adds them to the $declarations
* array following the format:
*
* array(
* 'name' => 'property_name',
* 'value' => 'property_value,
* )
*
* @since 5.8.0
*
* @param array $settings Settings to process.
* @return array The modified $declarations.
*/
protected static function compute_theme_vars( $settings ) {
$declarations = array();
$custom_values = isset( $settings['custom'] ) ? $settings['custom'] : array();
$css_vars = static::flatten_tree( $custom_values );
foreach ( $css_vars as $key => $value ) {
$declarations[] = array(
'name' => '--wp--custom--' . $key,
'value' => $value,
);
}
return $declarations;
}
/**
* Given a tree, it creates a flattened one
* by merging the keys and binding the leaf values
* to the new keys.
*
* It also transforms camelCase names into kebab-case
* and substitutes '/' by '-'.
*
* This is thought to be useful to generate
* CSS Custom Properties from a tree,
* although there's nothing in the implementation
* of this function that requires that format.
*
* For example, assuming the given prefix is '--wp'
* and the token is '--', for this input tree:
*
* {
* 'some/property': 'value',
* 'nestedProperty': {
* 'sub-property': 'value'
* }
* }
*
* it'll return this output:
*
* {
* '--wp--some-property': 'value',
* '--wp--nested-property--sub-property': 'value'
* }
*
* @since 5.8.0
*
* @param array $tree Input tree to process.
* @param string $prefix Optional. Prefix to prepend to each variable. Default empty string.
* @param string $token Optional. Token to use between levels. Default '--'.
* @return array The flattened tree.
*/
protected static function flatten_tree( $tree, $prefix = '', $token = '--' ) {
$result = array();
foreach ( $tree as $property => $value ) {
$new_key = $prefix . str_replace(
'/',
'-',
strtolower( _wp_to_kebab_case( $property ) )
);
if ( is_array( $value ) ) {
$new_prefix = $new_key . $token;
$flattened_subtree = static::flatten_tree( $value, $new_prefix, $token );
foreach ( $flattened_subtree as $subtree_key => $subtree_value ) {
$result[ $subtree_key ] = $subtree_value;
}
} else {
$result[ $new_key ] = $value;
}
}
return $result;
}
/**
* Given a styles array, it extracts the style properties
* and adds them to the $declarations array following the format:
*
* array(
* 'name' => 'property_name',
* 'value' => 'property_value',
* )
*
* @since 5.8.0
* @since 5.9.0 Added the `$settings` and `$properties` parameters.
* @since 6.1.0 Added `$theme_json`, `$selector`, and `$use_root_padding` parameters.
* @since 6.5.0 Output a `min-height: unset` rule when `aspect-ratio` is set.
* @since 6.6.0 Pass current theme JSON settings to wp_get_typography_font_size_value(), and process background properties.
* @since 6.7.0 `ref` resolution of background properties, and assigning custom default values.
*
* @param array $styles Styles to process.
* @param array $settings Theme settings.
* @param array $properties Properties metadata.
* @param array $theme_json Theme JSON array.
* @param string $selector The style block selector.
* @param boolean $use_root_padding Whether to add custom properties at root level.
* @return array Returns the modified $declarations.
*/
protected static function compute_style_properties( $styles, $settings = array(), $properties = null, $theme_json = null, $selector = null, $use_root_padding = null ) {
if ( empty( $styles ) ) {
return array();
}
if ( null === $properties ) {
$properties = static::PROPERTIES_METADATA;
}
$declarations = array();
$root_variable_duplicates = array();
$root_style_length = strlen( '--wp--style--root--' );
foreach ( $properties as $css_property => $value_path ) {
if ( ! is_array( $value_path ) ) {
continue;
}
$is_root_style = str_starts_with( $css_property, '--wp--style--root--' );
if ( $is_root_style && ( static::ROOT_BLOCK_SELECTOR !== $selector || ! $use_root_padding ) ) {
continue;
}
$value = static::get_property_value( $styles, $value_path, $theme_json );
/*
* Root-level padding styles don't currently support strings with CSS shorthand values.
* This may change: https://github.com/WordPress/gutenberg/issues/40132.
*/
if ( '--wp--style--root--padding' === $css_property && is_string( $value ) ) {
continue;
}
if ( $is_root_style && $use_root_padding ) {
$root_variable_duplicates[] = substr( $css_property, $root_style_length );
}
/*
* Processes background image styles.
* If the value is a URL, it will be converted to a CSS `url()` value.
* For uploaded image (images with a database ID), apply size and position defaults,
* equal to those applied in block supports in lib/background.php.
*/
if ( 'background-image' === $css_property && ! empty( $value ) ) {
$background_styles = wp_style_engine_get_styles(
array( 'background' => array( 'backgroundImage' => $value ) )
);
$value = $background_styles['declarations'][ $css_property ];
}
if ( empty( $value ) && static::ROOT_BLOCK_SELECTOR !== $selector && ! empty( $styles['background']['backgroundImage']['id'] ) ) {
if ( 'background-size' === $css_property ) {
$value = 'cover';
}
// If the background size is set to `contain` and no position is set, set the position to `center`.
if ( 'background-position' === $css_property ) {
$background_size = $styles['background']['backgroundSize'] ?? null;
$value = 'contain' === $background_size ? '50% 50%' : null;
}
}
// Skip if empty and not "0" or value represents array of longhand values.
$has_missing_value = empty( $value ) && ! is_numeric( $value );
if ( $has_missing_value || is_array( $value ) ) {
continue;
}
/*
* Look up protected properties, keyed by value path.
* Skip protected properties that are explicitly set to `null`.
*/
$path_string = implode( '.', $value_path );
if (
isset( static::PROTECTED_PROPERTIES[ $path_string ] ) &&
_wp_array_get( $settings, static::PROTECTED_PROPERTIES[ $path_string ], null ) === null
) {
continue;
}
// Calculates fluid typography rules where available.
if ( 'font-size' === $css_property ) {
/*
* wp_get_typography_font_size_value() will check
* if fluid typography has been activated and also
* whether the incoming value can be converted to a fluid value.
* Values that already have a clamp() function will not pass the test,
* and therefore the original $value will be returned.
* Pass the current theme_json settings to override any global settings.
*/
$value = wp_get_typography_font_size_value( array( 'size' => $value ), $settings );
}
if ( 'aspect-ratio' === $css_property ) {
// For aspect ratio to work, other dimensions rules must be unset.
// This ensures that a fixed height does not override the aspect ratio.
$declarations[] = array(
'name' => 'min-height',
'value' => 'unset',
);
}
$declarations[] = array(
'name' => $css_property,
'value' => $value,
);
}
// If a variable value is added to the root, the corresponding property should be removed.
foreach ( $root_variable_duplicates as $duplicate ) {
$discard = array_search( $duplicate, array_column( $declarations, 'name' ), true );
if ( is_numeric( $discard ) ) {
array_splice( $declarations, $discard, 1 );
}
}
return $declarations;
}
/**
* Returns the style property for the given path.
*
* It also converts references to a path to the value
* stored at that location, e.g.
* { "ref": "style.color.background" } => "#fff".
*
* @since 5.8.0
* @since 5.9.0 Added support for values of array type, which are returned as is.
* @since 6.1.0 Added the `$theme_json` parameter.
* @since 6.3.0 It no longer converts the internal format "var:preset|color|secondary"
* to the standard form "--wp--preset--color--secondary".
* This is already done by the sanitize method,
* so every property will be in the standard form.
* @since 6.7.0 Added support for background image refs.
*
* @param array $styles Styles subtree.
* @param array $path Which property to process.
* @param array $theme_json Theme JSON array.
* @return string|array Style property value.
*/
protected static function get_property_value( $styles, $path, $theme_json = null ) {
$value = _wp_array_get( $styles, $path, '' );
if ( '' === $value || null === $value ) {
// No need to process the value further.
return '';
}
/*
* This converts references to a path to the value at that path
* where the value is an array with a "ref" key, pointing to a path.
* For example: { "ref": "style.color.background" } => "#fff".
* In the case of backgroundImage, if both a ref and a URL are present in the value,
* the URL takes precedence and the ref is ignored.
*/
if ( is_array( $value ) && isset( $value['ref'] ) ) {
$value_path = explode( '.', $value['ref'] );
$ref_value = _wp_array_get( $theme_json, $value_path );
// Background Image refs can refer to a string or an array containing a URL string.
$ref_value_url = $ref_value['url'] ?? null;
// Only use the ref value if we find anything.
if ( ! empty( $ref_value ) && ( is_string( $ref_value ) || is_string( $ref_value_url ) ) ) {
$value = $ref_value;
}
if ( is_array( $ref_value ) && isset( $ref_value['ref'] ) ) {
$path_string = json_encode( $path );
$ref_value_string = json_encode( $ref_value );
_doing_it_wrong(
'get_property_value',
sprintf(
/* translators: 1: theme.json, 2: Value name, 3: Value path, 4: Another value name. */
__( 'Your %1$s file uses a dynamic value (%2$s) for the path at %3$s. However, the value at %3$s is also a dynamic value (pointing to %4$s) and pointing to another dynamic value is not supported. Please update %3$s to point directly to %4$s.' ),
'theme.json',
$ref_value_string,
$path_string,
$ref_value['ref']
),
'6.1.0'
);
}
}
if ( is_array( $value ) ) {
return $value;
}
return $value;
}
/**
* Builds metadata for the setting nodes, which returns in the form of:
*
* [
* [
* 'path' => ['path', 'to', 'some', 'node' ],
* 'selector' => 'CSS selector for some node'
* ],
* [
* 'path' => [ 'path', 'to', 'other', 'node' ],
* 'selector' => 'CSS selector for other node'
* ],
* ]
*
* @since 5.8.0
*
* @param array $theme_json The tree to extract setting nodes from.
* @param array $selectors List of selectors per block.
* @return array An array of setting nodes metadata.
*/
protected static function get_setting_nodes( $theme_json, $selectors = array() ) {
$nodes = array();
if ( ! isset( $theme_json['settings'] ) ) {
return $nodes;
}
// Top-level.
$nodes[] = array(
'path' => array( 'settings' ),
'selector' => static::ROOT_CSS_PROPERTIES_SELECTOR,
);
// Calculate paths for blocks.
if ( ! isset( $theme_json['settings']['blocks'] ) ) {
return $nodes;
}
foreach ( $theme_json['settings']['blocks'] as $name => $node ) {
$selector = null;
if ( isset( $selectors[ $name ]['selector'] ) ) {
$selector = $selectors[ $name ]['selector'];
}
$nodes[] = array(
'path' => array( 'settings', 'blocks', $name ),
'selector' => $selector,
);
}
return $nodes;
}
/**
* Builds metadata for the style nodes, which returns in the form of:
*
* [
* [
* 'path' => [ 'path', 'to', 'some', 'node' ],
* 'selector' => 'CSS selector for some node',
* 'duotone' => 'CSS selector for duotone for some node'
* ],
* [
* 'path' => ['path', 'to', 'other', 'node' ],
* 'selector' => 'CSS selector for other node',
* 'duotone' => null
* ],
* ]
*
* @since 5.8.0
* @since 6.6.0 Added options array for modifying generated nodes.
*
* @param array $theme_json The tree to extract style nodes from.
* @param array $selectors List of selectors per block.
* @param array $options {
* Optional. An array of options for now used for internal purposes only (may change without notice).
*
* @type bool $include_block_style_variations Includes style nodes for block style variations. Default false.
* }
* @return array An array of style nodes metadata.
*/
protected static function get_style_nodes( $theme_json, $selectors = array(), $options = array() ) {
$nodes = array();
if ( ! isset( $theme_json['styles'] ) ) {
return $nodes;
}
// Top-level.
$nodes[] = array(
'path' => array( 'styles' ),
'selector' => static::ROOT_BLOCK_SELECTOR,
);
if ( isset( $theme_json['styles']['elements'] ) ) {
foreach ( self::ELEMENTS as $element => $selector ) {
if ( ! isset( $theme_json['styles']['elements'][ $element ] ) ) {
continue;
}
$nodes[] = array(
'path' => array( 'styles', 'elements', $element ),
'selector' => static::ELEMENTS[ $element ],
);
// Handle any pseudo selectors for the element.
if ( isset( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $element ] ) ) {
foreach ( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $element ] as $pseudo_selector ) {
if ( isset( $theme_json['styles']['elements'][ $element ][ $pseudo_selector ] ) ) {
$nodes[] = array(
'path' => array( 'styles', 'elements', $element ),
'selector' => static::append_to_selector( static::ELEMENTS[ $element ], $pseudo_selector ),
);
}
}
}
}
}
// Blocks.
if ( ! isset( $theme_json['styles']['blocks'] ) ) {
return $nodes;
}
$block_nodes = static::get_block_nodes( $theme_json, $selectors, $options );
foreach ( $block_nodes as $block_node ) {
$nodes[] = $block_node;
}
/**
* Filters the list of style nodes with metadata.
*
* This allows for things like loading block CSS independently.
*
* @since 6.1.0
*
* @param array $nodes Style nodes with metadata.
*/
return apply_filters( 'wp_theme_json_get_style_nodes', $nodes );
}
/**
* A public helper to get the block nodes from a theme.json file.
*
* @since 6.1.0
*
* @return array The block nodes in theme.json.
*/
public function get_styles_block_nodes() {
return static::get_block_nodes( $this->theme_json );
}
/**
* Returns a filtered declarations array if there is a separator block with only a background
* style defined in theme.json by adding a color attribute to reflect the changes in the front.
*
* @since 6.1.1
*
* @param array $declarations List of declarations.
* @return array $declarations List of declarations filtered.
*/
private static function update_separator_declarations( $declarations ) {
$background_color = '';
$border_color_matches = false;
$text_color_matches = false;
foreach ( $declarations as $declaration ) {
if ( 'background-color' === $declaration['name'] && ! $background_color && isset( $declaration['value'] ) ) {
$background_color = $declaration['value'];
} elseif ( 'border-color' === $declaration['name'] ) {
$border_color_matches = true;
} elseif ( 'color' === $declaration['name'] ) {
$text_color_matches = true;
}
if ( $background_color && $border_color_matches && $text_color_matches ) {
break;
}
}
if ( $background_color && ! $border_color_matches && ! $text_color_matches ) {
$declarations[] = array(
'name' => 'color',
'value' => $background_color,
);
}
return $declarations;
}
/**
* An internal method to get the block nodes from a theme.json file.
*
* @since 6.1.0
* @since 6.3.0 Refactored and stabilized selectors API.
* @since 6.6.0 Added optional selectors and options for generating block nodes.
* @since 6.7.0 Added $include_node_paths_only option.
*
* @param array $theme_json The theme.json converted to an array.
* @param array $selectors Optional list of selectors per block.
* @param array $options {
* Optional. An array of options for now used for internal purposes only (may change without notice).
*
* @type bool $include_block_style_variations Include nodes for block style variations. Default false.
* @type bool $include_node_paths_only Return only block nodes node paths. Default false.
* }
* @return array The block nodes in theme.json.
*/
private static function get_block_nodes( $theme_json, $selectors = array(), $options = array() ) {
$nodes = array();
if ( ! isset( $theme_json['styles']['blocks'] ) ) {
return $nodes;
}
$include_variations = $options['include_block_style_variations'] ?? false;
$include_node_paths_only = $options['include_node_paths_only'] ?? false;
// If only node paths are to be returned, skip selector assignment.
if ( ! $include_node_paths_only ) {
$selectors = empty( $selectors ) ? static::get_blocks_metadata() : $selectors;
}
foreach ( $theme_json['styles']['blocks'] as $name => $node ) {
$node_path = array( 'styles', 'blocks', $name );
if ( $include_node_paths_only ) {
$variation_paths = array();
if ( $include_variations && isset( $node['variations'] ) ) {
foreach ( $node['variations'] as $variation => $variation_node ) {
$variation_paths[] = array(
'path' => array( 'styles', 'blocks', $name, 'variations', $variation ),
);
}
}
$node = array(
'path' => $node_path,
);
if ( ! empty( $variation_paths ) ) {
$node['variations'] = $variation_paths;
}
$nodes[] = $node;
} else {
$selector = null;
if ( isset( $selectors[ $name ]['selector'] ) ) {
$selector = $selectors[ $name ]['selector'];
}
$duotone_selector = null;
if ( isset( $selectors[ $name ]['duotone'] ) ) {
$duotone_selector = $selectors[ $name ]['duotone'];
}
$feature_selectors = null;
if ( isset( $selectors[ $name ]['selectors'] ) ) {
$feature_selectors = $selectors[ $name ]['selectors'];
}
$variation_selectors = array();
if ( $include_variations && isset( $node['variations'] ) ) {
foreach ( $node['variations'] as $variation => $node ) {
$variation_selectors[] = array(
'path' => array( 'styles', 'blocks', $name, 'variations', $variation ),
'selector' => $selectors[ $name ]['styleVariations'][ $variation ],
);
}
}
$nodes[] = array(
'name' => $name,
'path' => $node_path,
'selector' => $selector,
'selectors' => $feature_selectors,
'duotone' => $duotone_selector,
'features' => $feature_selectors,
'variations' => $variation_selectors,
'css' => $selector,
);
}
if ( isset( $theme_json['styles']['blocks'][ $name ]['elements'] ) ) {
foreach ( $theme_json['styles']['blocks'][ $name ]['elements'] as $element => $node ) {
$node_path = array( 'styles', 'blocks', $name, 'elements', $element );
if ( $include_node_paths_only ) {
$nodes[] = array(
'path' => $node_path,
);
continue;
}
$nodes[] = array(
'path' => $node_path,
'selector' => $selectors[ $name ]['elements'][ $element ],
);
// Handle any pseudo selectors for the element.
if ( isset( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $element ] ) ) {
foreach ( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $element ] as $pseudo_selector ) {
if ( isset( $theme_json['styles']['blocks'][ $name ]['elements'][ $element ][ $pseudo_selector ] ) ) {
$node_path = array( 'styles', 'blocks', $name, 'elements', $element );
$nodes[] = array(
'path' => $node_path,
'selector' => static::append_to_selector( $selectors[ $name ]['elements'][ $element ], $pseudo_selector ),
);
}
}
}
}
}
}
return $nodes;
}
/**
* Gets the CSS rules for a particular block from theme.json.
*
* @since 6.1.0
* @since 6.6.0 Setting a min-height of HTML when root styles have a background gradient or image.
* Updated general global styles specificity to 0-1-0.
* Fixed custom CSS output in block style variations.
*
* @param array $block_metadata Metadata about the block to get styles for.
* @return string Styles for the block.
*/
public function get_styles_for_block( $block_metadata ) {
$node = _wp_array_get( $this->theme_json, $block_metadata['path'], array() );
$use_root_padding = isset( $this->theme_json['settings']['useRootPaddingAwareAlignments'] ) && true === $this->theme_json['settings']['useRootPaddingAwareAlignments'];
$selector = $block_metadata['selector'];
$settings = isset( $this->theme_json['settings'] ) ? $this->theme_json['settings'] : array();
$feature_declarations = static::get_feature_declarations_for_node( $block_metadata, $node );
$is_root_selector = static::ROOT_BLOCK_SELECTOR === $selector;
// If there are style variations, generate the declarations for them, including any feature selectors the block may have.
$style_variation_declarations = array();
$style_variation_custom_css = array();
if ( ! empty( $block_metadata['variations'] ) ) {
foreach ( $block_metadata['variations'] as $style_variation ) {
$style_variation_node = _wp_array_get( $this->theme_json, $style_variation['path'], array() );
$clean_style_variation_selector = trim( $style_variation['selector'] );
// Generate any feature/subfeature style declarations for the current style variation.
$variation_declarations = static::get_feature_declarations_for_node( $block_metadata, $style_variation_node );
// Combine selectors with style variation's selector and add to overall style variation declarations.
foreach ( $variation_declarations as $current_selector => $new_declarations ) {
/*
* Clean up any whitespace between comma separated selectors.
* This prevents these spaces breaking compound selectors such as:
* - `.wp-block-list:not(.wp-block-list .wp-block-list)`
* - `.wp-block-image img, .wp-block-image.my-class img`
*/
$clean_current_selector = preg_replace( '/,\s+/', ',', $current_selector );
$shortened_selector = str_replace( $block_metadata['selector'], '', $clean_current_selector );
// Prepend the variation selector to the current selector.
$split_selectors = explode( ',', $shortened_selector );
$updated_selectors = array_map(
static function ( $split_selector ) use ( $clean_style_variation_selector ) {
return $clean_style_variation_selector . $split_selector;
},
$split_selectors
);
$combined_selectors = implode( ',', $updated_selectors );
// Add the new declarations to the overall results under the modified selector.
$style_variation_declarations[ $combined_selectors ] = $new_declarations;
}
// Compute declarations for remaining styles not covered by feature level selectors.
$style_variation_declarations[ $style_variation['selector'] ] = static::compute_style_properties( $style_variation_node, $settings, null, $this->theme_json );
// Store custom CSS for the style variation.
if ( isset( $style_variation_node['css'] ) ) {
$style_variation_custom_css[ $style_variation['selector'] ] = $this->process_blocks_custom_css( $style_variation_node['css'], $style_variation['selector'] );
}
}
}
/*
* Get a reference to element name from path.
* $block_metadata['path'] = array( 'styles','elements','link' );
* Make sure that $block_metadata['path'] describes an element node, like [ 'styles', 'element', 'link' ].
* Skip non-element paths like just ['styles'].
*/
$is_processing_element = in_array( 'elements', $block_metadata['path'], true );
$current_element = $is_processing_element ? $block_metadata['path'][ count( $block_metadata['path'] ) - 1 ] : null;
$element_pseudo_allowed = array();
if ( isset( $current_element, static::VALID_ELEMENT_PSEUDO_SELECTORS[ $current_element ] ) ) {
$element_pseudo_allowed = static::VALID_ELEMENT_PSEUDO_SELECTORS[ $current_element ];
}
/*
* Check for allowed pseudo classes (e.g. ":hover") from the $selector ("a:hover").
* This also resets the array keys.
*/
$pseudo_matches = array_values(
array_filter(
$element_pseudo_allowed,
static function ( $pseudo_selector ) use ( $selector ) {
/*
* Check if the pseudo selector is in the current selector,
* ensuring it is not followed by a dash (e.g., :focus should not match :focus-visible).
*/
return preg_match( '/' . preg_quote( $pseudo_selector, '/' ) . '(?!-)/', $selector ) === 1;
}
)
);
$pseudo_selector = isset( $pseudo_matches[0] ) ? $pseudo_matches[0] : null;
/*
* If the current selector is a pseudo selector that's defined in the allow list for the current
* element then compute the style properties for it.
* Otherwise just compute the styles for the default selector as normal.
*/
if ( $pseudo_selector && isset( $node[ $pseudo_selector ] ) &&
isset( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $current_element ] )
&& in_array( $pseudo_selector, static::VALID_ELEMENT_PSEUDO_SELECTORS[ $current_element ], true )
) {
$declarations = static::compute_style_properties( $node[ $pseudo_selector ], $settings, null, $this->theme_json, $selector, $use_root_padding );
} else {
$declarations = static::compute_style_properties( $node, $settings, null, $this->theme_json, $selector, $use_root_padding );
}
$block_rules = '';
/*
* 1. Bespoke declaration modifiers:
* - 'filter': Separate the declarations that use the general selector
* from the ones using the duotone selector.
* - 'background|background-image': set the html min-height to 100%
* to ensure the background covers the entire viewport.
*/
$declarations_duotone = array();
$should_set_root_min_height = false;
foreach ( $declarations as $index => $declaration ) {
if ( 'filter' === $declaration['name'] ) {
/*
* 'unset' filters happen when a filter is unset
* in the site-editor UI. Because the 'unset' value
* in the user origin overrides the value in the
* theme origin, we can skip rendering anything
* here as no filter needs to be applied anymore.
* So only add declarations to with values other
* than 'unset'.
*/
if ( 'unset' !== $declaration['value'] ) {
$declarations_duotone[] = $declaration;
}
unset( $declarations[ $index ] );
}
if ( $is_root_selector && ( 'background-image' === $declaration['name'] || 'background' === $declaration['name'] ) ) {
$should_set_root_min_height = true;
}
}
/*
* If root styles has a background-image or a background (gradient) set,
* set the min-height to '100%'. Minus `--wp-admin--admin-bar--height` for logged-in view.
* Setting the CSS rule on the HTML tag ensures background gradients and images behave similarly,
* and matches the behavior of the site editor.
*/
if ( $should_set_root_min_height ) {
$block_rules .= static::to_ruleset(
'html',
array(
array(
'name' => 'min-height',
'value' => 'calc(100% - var(--wp-admin--admin-bar--height, 0px))',
),
)
);
}
// Update declarations if there are separators with only background color defined.
if ( '.wp-block-separator' === $selector ) {
$declarations = static::update_separator_declarations( $declarations );
}
/*
* Root selector (body) styles should not be wrapped in `:root where()` to keep
* specificity at (0,0,1) and maintain backwards compatibility.
*
* Top-level element styles using element-only specificity selectors should
* not get wrapped in `:root :where()` to maintain backwards compatibility.
*
* Pseudo classes, e.g. :hover, :focus etc., are a class-level selector so
* still need to be wrapped in `:root :where` to cap specificity for nested
* variations etc. Pseudo selectors won't match the ELEMENTS selector exactly.
*/
$element_only_selector = $is_root_selector || (
$current_element &&
isset( static::ELEMENTS[ $current_element ] ) &&
// buttons, captions etc. still need `:root :where()` as they are class based selectors.
! isset( static::__EXPERIMENTAL_ELEMENT_CLASS_NAMES[ $current_element ] ) &&
static::ELEMENTS[ $current_element ] === $selector
);
// 2. Generate and append the rules that use the general selector.
$general_selector = $element_only_selector ? $selector : ":root :where($selector)";
$block_rules .= static::to_ruleset( $general_selector, $declarations );
// 3. Generate and append the rules that use the duotone selector.
if ( isset( $block_metadata['duotone'] ) && ! empty( $declarations_duotone ) ) {
$block_rules .= static::to_ruleset( $block_metadata['duotone'], $declarations_duotone );
}
// 4. Generate Layout block gap styles.
if (
! $is_root_selector &&
! empty( $block_metadata['name'] )
) {
$block_rules .= $this->get_layout_styles( $block_metadata );
}
// 5. Generate and append the feature level rulesets.
foreach ( $feature_declarations as $feature_selector => $individual_feature_declarations ) {
$block_rules .= static::to_ruleset( ":root :where($feature_selector)", $individual_feature_declarations );
}
// 6. Generate and append the style variation rulesets.
foreach ( $style_variation_declarations as $style_variation_selector => $individual_style_variation_declarations ) {
$block_rules .= static::to_ruleset( ":root :where($style_variation_selector)", $individual_style_variation_declarations );
if ( isset( $style_variation_custom_css[ $style_variation_selector ] ) ) {
$block_rules .= $style_variation_custom_css[ $style_variation_selector ];
}
}
// 7. Generate and append any custom CSS rules.
if ( isset( $node['css'] ) && ! $is_root_selector ) {
$block_rules .= $this->process_blocks_custom_css( $node['css'], $selector );
}
return $block_rules;
}
/**
* Outputs the CSS for layout rules on the root.
*
* @since 6.1.0
* @since 6.6.0 Use `ROOT_CSS_PROPERTIES_SELECTOR` for CSS custom properties and improved consistency of root padding rules.
* Updated specificity of body margin reset and first/last child selectors.
*
* @param string $selector The root node selector.
* @param array $block_metadata The metadata for the root block.
* @return string The additional root rules CSS.
*/
public function get_root_layout_rules( $selector, $block_metadata ) {
$css = '';
$settings = isset( $this->theme_json['settings'] ) ? $this->theme_json['settings'] : array();
$use_root_padding = isset( $this->theme_json['settings']['useRootPaddingAwareAlignments'] ) && true === $this->theme_json['settings']['useRootPaddingAwareAlignments'];
/*
* If there are content and wide widths in theme.json, output them
* as custom properties on the body element so all blocks can use them.
*/
if ( isset( $settings['layout']['contentSize'] ) || isset( $settings['layout']['wideSize'] ) ) {
$content_size = isset( $settings['layout']['contentSize'] ) ? $settings['layout']['contentSize'] : $settings['layout']['wideSize'];
$content_size = static::is_safe_css_declaration( 'max-width', $content_size ) ? $content_size : 'initial';
$wide_size = isset( $settings['layout']['wideSize'] ) ? $settings['layout']['wideSize'] : $settings['layout']['contentSize'];
$wide_size = static::is_safe_css_declaration( 'max-width', $wide_size ) ? $wide_size : 'initial';
$css .= static::ROOT_CSS_PROPERTIES_SELECTOR . ' { --wp--style--global--content-size: ' . $content_size . ';';
$css .= '--wp--style--global--wide-size: ' . $wide_size . '; }';
}
/*
* Reset default browser margin on the body element.
* This is set on the body selector **before** generating the ruleset
* from the `theme.json`. This is to ensure that if the `theme.json` declares
* `margin` in its `spacing` declaration for the `body` element then these
* user-generated values take precedence in the CSS cascade.
* @link https://github.com/WordPress/gutenberg/issues/36147.
*/
$css .= ':where(body) { margin: 0; }';
if ( $use_root_padding ) {
// Top and bottom padding are applied to the outer block container.
$css .= '.wp-site-blocks { padding-top: var(--wp--style--root--padding-top); padding-bottom: var(--wp--style--root--padding-bottom); }';
// Right and left padding are applied to the first container with `.has-global-padding` class.
$css .= '.has-global-padding { padding-right: var(--wp--style--root--padding-right); padding-left: var(--wp--style--root--padding-left); }';
// Alignfull children of the container with left and right padding have negative margins so they can still be full width.
$css .= '.has-global-padding > .alignfull { margin-right: calc(var(--wp--style--root--padding-right) * -1); margin-left: calc(var(--wp--style--root--padding-left) * -1); }';
// Nested children of the container with left and right padding that are not full aligned do not get padding, unless they are direct children of an alignfull flow container.
$css .= '.has-global-padding :where(:not(.alignfull.is-layout-flow) > .has-global-padding:not(.wp-block-block, .alignfull)) { padding-right: 0; padding-left: 0; }';
// Alignfull direct children of the containers that are targeted by the rule above do not need negative margins.
$css .= '.has-global-padding :where(:not(.alignfull.is-layout-flow) > .has-global-padding:not(.wp-block-block, .alignfull)) > .alignfull { margin-left: 0; margin-right: 0; }';
}
$css .= '.wp-site-blocks > .alignleft { float: left; margin-right: 2em; }';
$css .= '.wp-site-blocks > .alignright { float: right; margin-left: 2em; }';
$css .= '.wp-site-blocks > .aligncenter { justify-content: center; margin-left: auto; margin-right: auto; }';
// Block gap styles will be output unless explicitly set to `null`. See static::PROTECTED_PROPERTIES.
if ( isset( $this->theme_json['settings']['spacing']['blockGap'] ) ) {
$block_gap_value = static::get_property_value( $this->theme_json, array( 'styles', 'spacing', 'blockGap' ) );
$css .= ":where(.wp-site-blocks) > * { margin-block-start: $block_gap_value; margin-block-end: 0; }";
$css .= ':where(.wp-site-blocks) > :first-child { margin-block-start: 0; }';
$css .= ':where(.wp-site-blocks) > :last-child { margin-block-end: 0; }';
// For backwards compatibility, ensure the legacy block gap CSS variable is still available.
$css .= static::ROOT_CSS_PROPERTIES_SELECTOR . " { --wp--style--block-gap: $block_gap_value; }";
}
$css .= $this->get_layout_styles( $block_metadata );
return $css;
}
/**
* For metadata values that can either be booleans or paths to booleans, gets the value.
*
* $data = array(
* 'color' => array(
* 'defaultPalette' => true
* )
* );
*
* static::get_metadata_boolean( $data, false );
* // => false
*
* static::get_metadata_boolean( $data, array( 'color', 'defaultPalette' ) );
* // => true
*
* @since 6.0.0
*
* @param array $data The data to inspect.
* @param bool|array $path Boolean or path to a boolean.
* @param bool $default_value Default value if the referenced path is missing.
* Default false.
* @return bool Value of boolean metadata.
*/
protected static function get_metadata_boolean( $data, $path, $default_value = false ) {
if ( is_bool( $path ) ) {
return $path;
}
if ( is_array( $path ) ) {
$value = _wp_array_get( $data, $path );
if ( null !== $value ) {
return $value;
}
}
return $default_value;
}
/**
* Merges new incoming data.
*
* @since 5.8.0
* @since 5.9.0 Duotone preset also has origins.
* @since 6.7.0 Replace background image objects during merge.
*
* @param WP_Theme_JSON $incoming Data to merge.
*/
public function merge( $incoming ) {
$incoming_data = $incoming->get_raw_data();
$this->theme_json = array_replace_recursive( $this->theme_json, $incoming_data );
/*
* Recompute all the spacing sizes based on the new hierarchy of data. In the constructor
* spacingScale and spacingSizes are both keyed by origin and VALID_ORIGINS is ordered, so
* we can allow partial spacingScale data to inherit missing data from earlier layers when
* computing the spacing sizes.
*
* This happens before the presets are merged to ensure that default spacing sizes can be
* removed from the theme origin if $prevent_override is true.
*/
$flattened_spacing_scale = array();
foreach ( static::VALID_ORIGINS as $origin ) {
$scale_path = array( 'settings', 'spacing', 'spacingScale', $origin );
// Apply the base spacing scale to the current layer.
$base_spacing_scale = _wp_array_get( $this->theme_json, $scale_path, array() );
$flattened_spacing_scale = array_replace( $flattened_spacing_scale, $base_spacing_scale );
$spacing_scale = _wp_array_get( $incoming_data, $scale_path, null );
if ( ! isset( $spacing_scale ) ) {
continue;
}
// Allow partial scale settings by merging with lower layers.
$flattened_spacing_scale = array_replace( $flattened_spacing_scale, $spacing_scale );
// Generate and merge the scales for this layer.
$sizes_path = array( 'settings', 'spacing', 'spacingSizes', $origin );
$spacing_sizes = _wp_array_get( $incoming_data, $sizes_path, array() );
$spacing_scale_sizes = static::compute_spacing_sizes( $flattened_spacing_scale );
$merged_spacing_sizes = static::merge_spacing_sizes( $spacing_scale_sizes, $spacing_sizes );
_wp_array_set( $incoming_data, $sizes_path, $merged_spacing_sizes );
}
/*
* The array_replace_recursive algorithm merges at the leaf level,
* but we don't want leaf arrays to be merged, so we overwrite it.
*
* For leaf values that are sequential arrays it will use the numeric indexes for replacement.
* We rather replace the existing with the incoming value, if it exists.
* This is the case of spacing.units.
*
* For leaf values that are associative arrays it will merge them as expected.
* This is also not the behavior we want for the current associative arrays (presets).
* We rather replace the existing with the incoming value, if it exists.
* This happens, for example, when we merge data from theme.json upon existing
* theme supports or when we merge anything coming from the same source twice.
* This is the case of color.palette, color.gradients, color.duotone,
* typography.fontSizes, or typography.fontFamilies.
*
* Additionally, for some preset types, we also want to make sure the
* values they introduce don't conflict with default values. We do so
* by checking the incoming slugs for theme presets and compare them
* with the equivalent default presets: if a slug is present as a default
* we remove it from the theme presets.
*/
$nodes = static::get_setting_nodes( $incoming_data );
$slugs_global = static::get_default_slugs( $this->theme_json, array( 'settings' ) );
foreach ( $nodes as $node ) {
// Replace the spacing.units.
$path = $node['path'];
$path[] = 'spacing';
$path[] = 'units';
$content = _wp_array_get( $incoming_data, $path, null );
if ( isset( $content ) ) {
_wp_array_set( $this->theme_json, $path, $content );
}
// Replace the presets.
foreach ( static::PRESETS_METADATA as $preset_metadata ) {
$prevent_override = $preset_metadata['prevent_override'];
if ( is_array( $prevent_override ) ) {
$global_path = array_merge( array( 'settings' ), $prevent_override );
$global_value = _wp_array_get( $this->theme_json, $global_path, null );
$node_level_path = array_merge( $node['path'], $prevent_override );
$prevent_override = _wp_array_get( $this->theme_json, $node_level_path, $global_value );
}
foreach ( static::VALID_ORIGINS as $origin ) {
$base_path = $node['path'];
foreach ( $preset_metadata['path'] as $leaf ) {
$base_path[] = $leaf;
}
$path = $base_path;
$path[] = $origin;
$content = _wp_array_get( $incoming_data, $path, null );
if ( ! isset( $content ) ) {
continue;
}
// Set names for theme presets based on the slug if they are not set and can use default names.
if ( 'theme' === $origin && $preset_metadata['use_default_names'] ) {
foreach ( $content as $key => $item ) {
if ( ! isset( $item['name'] ) ) {
$name = static::get_name_from_defaults( $item['slug'], $base_path );
if ( null !== $name ) {
$content[ $key ]['name'] = $name;
}
}
}
}
// Filter out default slugs from theme presets when defaults should not be overridden.
if ( 'theme' === $origin && $prevent_override ) {
$slugs_node = static::get_default_slugs( $this->theme_json, $node['path'] );
$preset_global = _wp_array_get( $slugs_global, $preset_metadata['path'], array() );
$preset_node = _wp_array_get( $slugs_node, $preset_metadata['path'], array() );
$preset_slugs = array_merge_recursive( $preset_global, $preset_node );
$content = static::filter_slugs( $content, $preset_slugs );
}
_wp_array_set( $this->theme_json, $path, $content );
}
}
}
/*
* Style values are merged at the leaf level, however
* some values provide exceptions, namely style values that are
* objects and represent unique definitions for the style.
*/
$style_nodes = static::get_block_nodes(
$this->theme_json,
array(),
array( 'include_node_paths_only' => true )
);
// Add top-level styles.
$style_nodes[] = array( 'path' => array( 'styles' ) );
foreach ( $style_nodes as $style_node ) {
$path = $style_node['path'];
/*
* Background image styles should be replaced, not merged,
* as they themselves are specific object definitions for the style.
*/
$background_image_path = array_merge( $path, static::PROPERTIES_METADATA['background-image'] );
$content = _wp_array_get( $incoming_data, $background_image_path, null );
if ( isset( $content ) ) {
_wp_array_set( $this->theme_json, $background_image_path, $content );
}
}
}
/**
* Converts all filter (duotone) presets into SVGs.
*
* @since 5.9.1
*
* @param array $origins List of origins to process.
* @return string SVG filters.
*/
public function get_svg_filters( $origins ) {
$blocks_metadata = static::get_blocks_metadata();
$setting_nodes = static::get_setting_nodes( $this->theme_json, $blocks_metadata );
$filters = '';
foreach ( $setting_nodes as $metadata ) {
$node = _wp_array_get( $this->theme_json, $metadata['path'], array() );
if ( empty( $node['color']['duotone'] ) ) {
continue;
}
$duotone_presets = $node['color']['duotone'];
foreach ( $origins as $origin ) {
if ( ! isset( $duotone_presets[ $origin ] ) ) {
continue;
}
foreach ( $duotone_presets[ $origin ] as $duotone_preset ) {
$filters .= WP_Duotone::get_filter_svg_from_preset( $duotone_preset );
}
}
}
return $filters;
}
/**
* Determines whether a presets should be overridden or not.
*
* @since 5.9.0
* @deprecated 6.0.0 Use {@see 'get_metadata_boolean'} instead.
*
* @param array $theme_json The theme.json like structure to inspect.
* @param array $path Path to inspect.
* @param bool|array $override Data to compute whether to override the preset.
* @return bool
*/
protected static function should_override_preset( $theme_json, $path, $override ) {
_deprecated_function( __METHOD__, '6.0.0', 'get_metadata_boolean' );
if ( is_bool( $override ) ) {
return $override;
}
/*
* The relationship between whether to override the defaults
* and whether the defaults are enabled is inverse:
*
* - If defaults are enabled => theme presets should not be overridden
* - If defaults are disabled => theme presets should be overridden
*
* For example, a theme sets defaultPalette to false,
* making the default palette hidden from the user.
* In that case, we want all the theme presets to be present,
* so they should override the defaults.
*/
if ( is_array( $override ) ) {
$value = _wp_array_get( $theme_json, array_merge( $path, $override ) );
if ( isset( $value ) ) {
return ! $value;
}
// Search the top-level key if none was found for this node.
$value = _wp_array_get( $theme_json, array_merge( array( 'settings' ), $override ) );
if ( isset( $value ) ) {
return ! $value;
}
return true;
}
}
/**
* Returns the default slugs for all the presets in an associative array
* whose keys are the preset paths and the leaves is the list of slugs.
*
* For example:
*
* array(
* 'color' => array(
* 'palette' => array( 'slug-1', 'slug-2' ),
* 'gradients' => array( 'slug-3', 'slug-4' ),
* ),
* )
*
* @since 5.9.0
*
* @param array $data A theme.json like structure.
* @param array $node_path The path to inspect. It's 'settings' by default.
* @return array
*/
protected static function get_default_slugs( $data, $node_path ) {
$slugs = array();
foreach ( static::PRESETS_METADATA as $metadata ) {
$path = $node_path;
foreach ( $metadata['path'] as $leaf ) {
$path[] = $leaf;
}
$path[] = 'default';
$preset = _wp_array_get( $data, $path, null );
if ( ! isset( $preset ) ) {
continue;
}
$slugs_for_preset = array();
foreach ( $preset as $item ) {
if ( isset( $item['slug'] ) ) {
$slugs_for_preset[] = $item['slug'];
}
}
_wp_array_set( $slugs, $metadata['path'], $slugs_for_preset );
}
return $slugs;
}
/**
* Gets a `default`'s preset name by a provided slug.
*
* @since 5.9.0
*
* @param string $slug The slug we want to find a match from default presets.
* @param array $base_path The path to inspect. It's 'settings' by default.
* @return string|null
*/
protected function get_name_from_defaults( $slug, $base_path ) {
$path = $base_path;
$path[] = 'default';
$default_content = _wp_array_get( $this->theme_json, $path, null );
if ( ! $default_content ) {
return null;
}
foreach ( $default_content as $item ) {
if ( $slug === $item['slug'] ) {
return $item['name'];
}
}
return null;
}
/**
* Removes the preset values whose slug is equal to any of given slugs.
*
* @since 5.9.0
*
* @param array $node The node with the presets to validate.
* @param array $slugs The slugs that should not be overridden.
* @return array The new node.
*/
protected static function filter_slugs( $node, $slugs ) {
if ( empty( $slugs ) ) {
return $node;
}
$new_node = array();
foreach ( $node as $value ) {
if ( isset( $value['slug'] ) && ! in_array( $value['slug'], $slugs, true ) ) {
$new_node[] = $value;
}
}
return $new_node;
}
/**
* Removes insecure data from theme.json.
*
* @since 5.9.0
* @since 6.3.2 Preserves global styles block variations when securing styles.
* @since 6.6.0 Updated to allow variation element styles and $origin parameter.
*
* @param array $theme_json Structure to sanitize.
* @param string $origin Optional. What source of data this object represents.
* One of 'blocks', 'default', 'theme', or 'custom'. Default 'theme'.
* @return array Sanitized structure.
*/
public static function remove_insecure_properties( $theme_json, $origin = 'theme' ) {
if ( ! in_array( $origin, static::VALID_ORIGINS, true ) ) {
$origin = 'theme';
}
$sanitized = array();
$theme_json = WP_Theme_JSON_Schema::migrate( $theme_json, $origin );
$blocks_metadata = static::get_blocks_metadata();
$valid_block_names = array_keys( $blocks_metadata );
$valid_element_names = array_keys( static::ELEMENTS );
$valid_variations = static::get_valid_block_style_variations( $blocks_metadata );
$theme_json = static::sanitize( $theme_json, $valid_block_names, $valid_element_names, $valid_variations );
$blocks_metadata = static::get_blocks_metadata();
$style_options = array( 'include_block_style_variations' => true ); // Allow variations data.
$style_nodes = static::get_style_nodes( $theme_json, $blocks_metadata, $style_options );
foreach ( $style_nodes as $metadata ) {
$input = _wp_array_get( $theme_json, $metadata['path'], array() );
if ( empty( $input ) ) {
continue;
}
// The global styles custom CSS is not sanitized, but can only be edited by users with 'edit_css' capability.
if ( isset( $input['css'] ) && current_user_can( 'edit_css' ) ) {
$output = $input;
} else {
$output = static::remove_insecure_styles( $input );
}
/*
* Get a reference to element name from path.
* $metadata['path'] = array( 'styles', 'elements', 'link' );
*/
$current_element = $metadata['path'][ count( $metadata['path'] ) - 1 ];
/*
* $output is stripped of pseudo selectors. Re-add and process them
* or insecure styles here.
*/
if ( isset( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $current_element ] ) ) {
foreach ( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $current_element ] as $pseudo_selector ) {
if ( isset( $input[ $pseudo_selector ] ) ) {
$output[ $pseudo_selector ] = static::remove_insecure_styles( $input[ $pseudo_selector ] );
}
}
}
if ( ! empty( $output ) ) {
_wp_array_set( $sanitized, $metadata['path'], $output );
}
if ( isset( $metadata['variations'] ) ) {
foreach ( $metadata['variations'] as $variation ) {
$variation_input = _wp_array_get( $theme_json, $variation['path'], array() );
if ( empty( $variation_input ) ) {
continue;
}
$variation_output = static::remove_insecure_styles( $variation_input );
if ( isset( $variation_input['blocks'] ) ) {
$variation_output['blocks'] = static::remove_insecure_inner_block_styles( $variation_input['blocks'] );
}
if ( isset( $variation_input['elements'] ) ) {
$variation_output['elements'] = static::remove_insecure_element_styles( $variation_input['elements'] );
}
if ( ! empty( $variation_output ) ) {
_wp_array_set( $sanitized, $variation['path'], $variation_output );
}
}
}
}
$setting_nodes = static::get_setting_nodes( $theme_json );
foreach ( $setting_nodes as $metadata ) {
$input = _wp_array_get( $theme_json, $metadata['path'], array() );
if ( empty( $input ) ) {
continue;
}
$output = static::remove_insecure_settings( $input );
if ( ! empty( $output ) ) {
_wp_array_set( $sanitized, $metadata['path'], $output );
}
}
if ( empty( $sanitized['styles'] ) ) {
unset( $theme_json['styles'] );
} else {
$theme_json['styles'] = $sanitized['styles'];
}
if ( empty( $sanitized['settings'] ) ) {
unset( $theme_json['settings'] );
} else {
$theme_json['settings'] = $sanitized['settings'];
}
return $theme_json;
}
/**
* Remove insecure element styles within a variation or block.
*
* @since 6.8.0
*
* @param array $elements The elements to process.
* @return array The sanitized elements styles.
*/
protected static function remove_insecure_element_styles( $elements ) {
$sanitized = array();
$valid_element_names = array_keys( static::ELEMENTS );
foreach ( $valid_element_names as $element_name ) {
$element_input = $elements[ $element_name ] ?? null;
if ( $element_input ) {
$element_output = static::remove_insecure_styles( $element_input );
if ( isset( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $element_name ] ) ) {
foreach ( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $element_name ] as $pseudo_selector ) {
if ( isset( $element_input[ $pseudo_selector ] ) ) {
$element_output[ $pseudo_selector ] = static::remove_insecure_styles( $element_input[ $pseudo_selector ] );
}
}
}
$sanitized[ $element_name ] = $element_output;
}
}
return $sanitized;
}
/**
* Remove insecure styles from inner blocks and their elements.
*
* @since 6.8.0
*
* @param array $blocks The block styles to process.
* @return array Sanitized block type styles.
*/
protected static function remove_insecure_inner_block_styles( $blocks ) {
$sanitized = array();
foreach ( $blocks as $block_type => $block_input ) {
$block_output = static::remove_insecure_styles( $block_input );
if ( isset( $block_input['elements'] ) ) {
$block_output['elements'] = static::remove_insecure_element_styles( $block_input['elements'] );
}
$sanitized[ $block_type ] = $block_output;
}
return $sanitized;
}
/**
* Processes a setting node and returns the same node
* without the insecure settings.
*
* @since 5.9.0
*
* @param array $input Node to process.
* @return array
*/
protected static function remove_insecure_settings( $input ) {
$output = array();
foreach ( static::PRESETS_METADATA as $preset_metadata ) {
foreach ( static::VALID_ORIGINS as $origin ) {
$path_with_origin = $preset_metadata['path'];
$path_with_origin[] = $origin;
$presets = _wp_array_get( $input, $path_with_origin, null );
if ( null === $presets ) {
continue;
}
$escaped_preset = array();
foreach ( $presets as $preset ) {
if (
esc_attr( esc_html( $preset['name'] ) ) === $preset['name'] &&
sanitize_html_class( $preset['slug'] ) === $preset['slug']
) {
$value = null;
if ( isset( $preset_metadata['value_key'], $preset[ $preset_metadata['value_key'] ] ) ) {
$value = $preset[ $preset_metadata['value_key'] ];
} elseif (
isset( $preset_metadata['value_func'] ) &&
is_callable( $preset_metadata['value_func'] )
) {
$value = call_user_func( $preset_metadata['value_func'], $preset );
}
$preset_is_valid = true;
foreach ( $preset_metadata['properties'] as $property ) {
if ( ! static::is_safe_css_declaration( $property, $value ) ) {
$preset_is_valid = false;
break;
}
}
if ( $preset_is_valid ) {
$escaped_preset[] = $preset;
}
}
}
if ( ! empty( $escaped_preset ) ) {
_wp_array_set( $output, $path_with_origin, $escaped_preset );
}
}
}
// Ensure indirect properties not included in any `PRESETS_METADATA` value are allowed.
static::remove_indirect_properties( $input, $output );
return $output;
}
/**
* Processes a style node and returns the same node
* without the insecure styles.
*
* @since 5.9.0
*
* @param array $input Node to process.
* @return array
*/
protected static function remove_insecure_styles( $input ) {
$output = array();
$declarations = static::compute_style_properties( $input );
foreach ( $declarations as $declaration ) {
if ( static::is_safe_css_declaration( $declaration['name'], $declaration['value'] ) ) {
$path = static::PROPERTIES_METADATA[ $declaration['name'] ];
/*
* Check the value isn't an array before adding so as to not
* double up shorthand and longhand styles.
*/
$value = _wp_array_get( $input, $path, array() );
if ( ! is_array( $value ) ) {
_wp_array_set( $output, $path, $value );
}
}
}
// Ensure indirect properties not handled by `compute_style_properties` are allowed.
static::remove_indirect_properties( $input, $output );
return $output;
}
/**
* Checks that a declaration provided by the user is safe.
*
* @since 5.9.0
*
* @param string $property_name Property name in a CSS declaration, i.e. the `color` in `color: red`.
* @param string $property_value Value in a CSS declaration, i.e. the `red` in `color: red`.
* @return bool
*/
protected static function is_safe_css_declaration( $property_name, $property_value ) {
$style_to_validate = $property_name . ': ' . $property_value;
$filtered = esc_html( safecss_filter_attr( $style_to_validate ) );
return ! empty( trim( $filtered ) );
}
/**
* Removes indirect properties from the given input node and
* sets in the given output node.
*
* @since 6.2.0
*
* @param array $input Node to process.
* @param array $output The processed node. Passed by reference.
*/
private static function remove_indirect_properties( $input, &$output ) {
foreach ( static::INDIRECT_PROPERTIES_METADATA as $property => $paths ) {
foreach ( $paths as $path ) {
$value = _wp_array_get( $input, $path );
if (
is_string( $value ) &&
static::is_safe_css_declaration( $property, $value )
) {
_wp_array_set( $output, $path, $value );
}
}
}
}
/**
* Returns the raw data.
*
* @since 5.8.0
*
* @return array Raw data.
*/
public function get_raw_data() {
return $this->theme_json;
}
/**
* Transforms the given editor settings according the
* add_theme_support format to the theme.json format.
*
* @since 5.8.0
*
* @param array $settings Existing editor settings.
* @return array Config that adheres to the theme.json schema.
*/
public static function get_from_editor_settings( $settings ) {
$theme_settings = array(
'version' => static::LATEST_SCHEMA,
'settings' => array(),
);
// Deprecated theme supports.
if ( isset( $settings['disableCustomColors'] ) ) {
$theme_settings['settings']['color']['custom'] = ! $settings['disableCustomColors'];
}
if ( isset( $settings['disableCustomGradients'] ) ) {
$theme_settings['settings']['color']['customGradient'] = ! $settings['disableCustomGradients'];
}
if ( isset( $settings['disableCustomFontSizes'] ) ) {
$theme_settings['settings']['typography']['customFontSize'] = ! $settings['disableCustomFontSizes'];
}
if ( isset( $settings['enableCustomLineHeight'] ) ) {
$theme_settings['settings']['typography']['lineHeight'] = $settings['enableCustomLineHeight'];
}
if ( isset( $settings['enableCustomUnits'] ) ) {
$theme_settings['settings']['spacing']['units'] = ( true === $settings['enableCustomUnits'] ) ?
array( 'px', 'em', 'rem', 'vh', 'vw', '%' ) :
$settings['enableCustomUnits'];
}
if ( isset( $settings['colors'] ) ) {
$theme_settings['settings']['color']['palette'] = $settings['colors'];
}
if ( isset( $settings['gradients'] ) ) {
$theme_settings['settings']['color']['gradients'] = $settings['gradients'];
}
if ( isset( $settings['fontSizes'] ) ) {
$font_sizes = $settings['fontSizes'];
// Back-compatibility for presets without units.
foreach ( $font_sizes as $key => $font_size ) {
if ( is_numeric( $font_size['size'] ) ) {
$font_sizes[ $key ]['size'] = $font_size['size'] . 'px';
}
}
$theme_settings['settings']['typography']['fontSizes'] = $font_sizes;
}
if ( isset( $settings['enableCustomSpacing'] ) ) {
$theme_settings['settings']['spacing']['padding'] = $settings['enableCustomSpacing'];
}
if ( isset( $settings['spacingSizes'] ) ) {
$theme_settings['settings']['spacing']['spacingSizes'] = $settings['spacingSizes'];
}
return $theme_settings;
}
/**
* Returns the current theme's wanted patterns(slugs) to be
* registered from Pattern Directory.
*
* @since 6.0.0
*
* @return string[]
*/
public function get_patterns() {
if ( isset( $this->theme_json['patterns'] ) && is_array( $this->theme_json['patterns'] ) ) {
return $this->theme_json['patterns'];
}
return array();
}
/**
* Returns a valid theme.json as provided by a theme.
*
* Unlike get_raw_data() this returns the presets flattened, as provided by a theme.
* This also uses appearanceTools instead of their opt-ins if all of them are true.
*
* @since 6.0.0
*
* @return array
*/
public function get_data() {
$output = $this->theme_json;
$nodes = static::get_setting_nodes( $output );
/**
* Flatten the theme & custom origins into a single one.
*
* For example, the following:
*
* {
* "settings": {
* "color": {
* "palette": {
* "theme": [ {} ],
* "custom": [ {} ]
* }
* }
* }
* }
*
* will be converted to:
*
* {
* "settings": {
* "color": {
* "palette": [ {} ]
* }
* }
* }
*/
foreach ( $nodes as $node ) {
foreach ( static::PRESETS_METADATA as $preset_metadata ) {
$path = $node['path'];
foreach ( $preset_metadata['path'] as $preset_metadata_path ) {
$path[] = $preset_metadata_path;
}
$preset = _wp_array_get( $output, $path, null );
if ( null === $preset ) {
continue;
}
$items = array();
if ( isset( $preset['theme'] ) ) {
foreach ( $preset['theme'] as $item ) {
$slug = $item['slug'];
unset( $item['slug'] );
$items[ $slug ] = $item;
}
}
if ( isset( $preset['custom'] ) ) {
foreach ( $preset['custom'] as $item ) {
$slug = $item['slug'];
unset( $item['slug'] );
$items[ $slug ] = $item;
}
}
$flattened_preset = array();
foreach ( $items as $slug => $value ) {
$flattened_preset[] = array_merge( array( 'slug' => (string) $slug ), $value );
}
_wp_array_set( $output, $path, $flattened_preset );
}
}
/*
* If all of the static::APPEARANCE_TOOLS_OPT_INS are true,
* this code unsets them and sets 'appearanceTools' instead.
*/
foreach ( $nodes as $node ) {
$all_opt_ins_are_set = true;
foreach ( static::APPEARANCE_TOOLS_OPT_INS as $opt_in_path ) {
$full_path = $node['path'];
foreach ( $opt_in_path as $opt_in_path_item ) {
$full_path[] = $opt_in_path_item;
}
/*
* Use "unset prop" as a marker instead of "null" because
* "null" can be a valid value for some props (e.g. blockGap).
*/
$opt_in_value = _wp_array_get( $output, $full_path, 'unset prop' );
if ( 'unset prop' === $opt_in_value ) {
$all_opt_ins_are_set = false;
break;
}
}
if ( $all_opt_ins_are_set ) {
$node_path_with_appearance_tools = $node['path'];
$node_path_with_appearance_tools[] = 'appearanceTools';
_wp_array_set( $output, $node_path_with_appearance_tools, true );
foreach ( static::APPEARANCE_TOOLS_OPT_INS as $opt_in_path ) {
$full_path = $node['path'];
foreach ( $opt_in_path as $opt_in_path_item ) {
$full_path[] = $opt_in_path_item;
}
/*
* Use "unset prop" as a marker instead of "null" because
* "null" can be a valid value for some props (e.g. blockGap).
*/
$opt_in_value = _wp_array_get( $output, $full_path, 'unset prop' );
if ( true !== $opt_in_value ) {
continue;
}
/*
* The following could be improved to be path independent.
* At the moment it relies on a couple of assumptions:
*
* - all opt-ins having a path of size 2.
* - there's two sources of settings: the top-level and the block-level.
*/
if (
( 1 === count( $node['path'] ) ) &&
( 'settings' === $node['path'][0] )
) {
// Top-level settings.
unset( $output['settings'][ $opt_in_path[0] ][ $opt_in_path[1] ] );
if ( empty( $output['settings'][ $opt_in_path[0] ] ) ) {
unset( $output['settings'][ $opt_in_path[0] ] );
}
} elseif (
( 3 === count( $node['path'] ) ) &&
( 'settings' === $node['path'][0] ) &&
( 'blocks' === $node['path'][1] )
) {
// Block-level settings.
$block_name = $node['path'][2];
unset( $output['settings']['blocks'][ $block_name ][ $opt_in_path[0] ][ $opt_in_path[1] ] );
if ( empty( $output['settings']['blocks'][ $block_name ][ $opt_in_path[0] ] ) ) {
unset( $output['settings']['blocks'][ $block_name ][ $opt_in_path[0] ] );
}
}
}
}
}
wp_recursive_ksort( $output );
return $output;
}
/**
* Sets the spacingSizes array based on the spacingScale values from theme.json.
*
* @since 6.1.0
* @deprecated 6.6.0 No longer used as the spacingSizes are automatically
* generated in the constructor and merge methods instead
* of manually after instantiation.
*
* @return null|void
*/
public function set_spacing_sizes() {
_deprecated_function( __METHOD__, '6.6.0' );
$spacing_scale = isset( $this->theme_json['settings']['spacing']['spacingScale'] )
? $this->theme_json['settings']['spacing']['spacingScale']
: array();
if ( ! isset( $spacing_scale['steps'] )
|| ! is_numeric( $spacing_scale['steps'] )
|| ! isset( $spacing_scale['mediumStep'] )
|| ! isset( $spacing_scale['unit'] )
|| ! isset( $spacing_scale['operator'] )
|| ! isset( $spacing_scale['increment'] )
|| ! isset( $spacing_scale['steps'] )
|| ! is_numeric( $spacing_scale['increment'] )
|| ! is_numeric( $spacing_scale['mediumStep'] )
|| ( '+' !== $spacing_scale['operator'] && '*' !== $spacing_scale['operator'] ) ) {
if ( ! empty( $spacing_scale ) ) {
wp_trigger_error(
__METHOD__,
sprintf(
/* translators: 1: theme.json, 2: settings.spacing.spacingScale */
__( 'Some of the %1$s %2$s values are invalid' ),
'theme.json',
'settings.spacing.spacingScale'
),
E_USER_NOTICE
);
}
return null;
}
// If theme authors want to prevent the generation of the core spacing scale they can set their theme.json spacingScale.steps to 0.
if ( 0 === $spacing_scale['steps'] ) {
return null;
}
$spacing_sizes = static::compute_spacing_sizes( $spacing_scale );
// If there are 7 or fewer steps in the scale revert to numbers for labels instead of t-shirt sizes.
if ( $spacing_scale['steps'] <= 7 ) {
for ( $spacing_sizes_count = 0; $spacing_sizes_count < count( $spacing_sizes ); $spacing_sizes_count++ ) {
$spacing_sizes[ $spacing_sizes_count ]['name'] = (string) ( $spacing_sizes_count + 1 );
}
}
_wp_array_set( $this->theme_json, array( 'settings', 'spacing', 'spacingSizes', 'default' ), $spacing_sizes );
}
/**
* Merges two sets of spacing size presets.
*
* @since 6.6.0
*
* @param array $base The base set of spacing sizes.
* @param array $incoming The set of spacing sizes to merge with the base. Duplicate slugs will override the base values.
* @return array The merged set of spacing sizes.
*/
private static function merge_spacing_sizes( $base, $incoming ) {
// Preserve the order if there are no base (spacingScale) values.
if ( empty( $base ) ) {
return $incoming;
}
$merged = array();
foreach ( $base as $item ) {
$merged[ $item['slug'] ] = $item;
}
foreach ( $incoming as $item ) {
$merged[ $item['slug'] ] = $item;
}
ksort( $merged, SORT_NUMERIC );
return array_values( $merged );
}
/**
* Generates a set of spacing sizes by starting with a medium size and
* applying an operator with an increment value to generate the rest of the
* sizes outward from the medium size. The medium slug is '50' with the rest
* of the slugs being 10 apart. The generated names use t-shirt sizing.
*
* Example:
*
* $spacing_scale = array(
* 'steps' => 4,
* 'mediumStep' => 16,
* 'unit' => 'px',
* 'operator' => '+',
* 'increment' => 2,
* );
* $spacing_sizes = static::compute_spacing_sizes( $spacing_scale );
* // -> array(
* // array( 'name' => 'Small', 'slug' => '40', 'size' => '14px' ),
* // array( 'name' => 'Medium', 'slug' => '50', 'size' => '16px' ),
* // array( 'name' => 'Large', 'slug' => '60', 'size' => '18px' ),
* // array( 'name' => 'X-Large', 'slug' => '70', 'size' => '20px' ),
* // )
*
* @since 6.6.0
*
* @param array $spacing_scale {
* The spacing scale values. All are required.
*
* @type int $steps The number of steps in the scale. (up to 10 steps are supported.)
* @type float $mediumStep The middle value that gets the slug '50'. (For even number of steps, this becomes the first middle value.)
* @type string $unit The CSS unit to use for the sizes.
* @type string $operator The mathematical operator to apply to generate the other sizes. Either '+' or '*'.
* @type float $increment The value used with the operator to generate the other sizes.
* }
* @return array The spacing sizes presets or an empty array if some spacing scale values are missing or invalid.
*/
private static function compute_spacing_sizes( $spacing_scale ) {
/*
* This condition is intentionally missing some checks on ranges for the values in order to
* keep backwards compatibility with the previous implementation.
*/
if (
! isset( $spacing_scale['steps'] ) ||
! is_numeric( $spacing_scale['steps'] ) ||
0 === $spacing_scale['steps'] ||
! isset( $spacing_scale['mediumStep'] ) ||
! is_numeric( $spacing_scale['mediumStep'] ) ||
! isset( $spacing_scale['unit'] ) ||
! isset( $spacing_scale['operator'] ) ||
( '+' !== $spacing_scale['operator'] && '*' !== $spacing_scale['operator'] ) ||
! isset( $spacing_scale['increment'] ) ||
! is_numeric( $spacing_scale['increment'] )
) {
return array();
}
$unit = '%' === $spacing_scale['unit'] ? '%' : sanitize_title( $spacing_scale['unit'] );
$current_step = $spacing_scale['mediumStep'];
$steps_mid_point = round( $spacing_scale['steps'] / 2, 0 );
$x_small_count = null;
$below_sizes = array();
$slug = 40;
$remainder = 0;
for ( $below_midpoint_count = $steps_mid_point - 1; $spacing_scale['steps'] > 1 && $slug > 0 && $below_midpoint_count > 0; $below_midpoint_count-- ) {
if ( '+' === $spacing_scale['operator'] ) {
$current_step -= $spacing_scale['increment'];
} elseif ( $spacing_scale['increment'] > 1 ) {
$current_step /= $spacing_scale['increment'];
} else {
$current_step *= $spacing_scale['increment'];
}
if ( $current_step <= 0 ) {
$remainder = $below_midpoint_count;
break;
}
$below_sizes[] = array(
/* translators: %s: Digit to indicate multiple of sizing, eg. 2X-Small. */
'name' => $below_midpoint_count === $steps_mid_point - 1 ? __( 'Small' ) : sprintf( __( '%sX-Small' ), (string) $x_small_count ),
'slug' => (string) $slug,
'size' => round( $current_step, 2 ) . $unit,
);
if ( $below_midpoint_count === $steps_mid_point - 2 ) {
$x_small_count = 2;
}
if ( $below_midpoint_count < $steps_mid_point - 2 ) {
++$x_small_count;
}
$slug -= 10;
}
$below_sizes = array_reverse( $below_sizes );
$below_sizes[] = array(
'name' => __( 'Medium' ),
'slug' => '50',
'size' => $spacing_scale['mediumStep'] . $unit,
);
$current_step = $spacing_scale['mediumStep'];
$x_large_count = null;
$above_sizes = array();
$slug = 60;
$steps_above = ( $spacing_scale['steps'] - $steps_mid_point ) + $remainder;
for ( $above_midpoint_count = 0; $above_midpoint_count < $steps_above; $above_midpoint_count++ ) {
$current_step = '+' === $spacing_scale['operator']
? $current_step + $spacing_scale['increment']
: ( $spacing_scale['increment'] >= 1 ? $current_step * $spacing_scale['increment'] : $current_step / $spacing_scale['increment'] );
$above_sizes[] = array(
/* translators: %s: Digit to indicate multiple of sizing, eg. 2X-Large. */
'name' => 0 === $above_midpoint_count ? __( 'Large' ) : sprintf( __( '%sX-Large' ), (string) $x_large_count ),
'slug' => (string) $slug,
'size' => round( $current_step, 2 ) . $unit,
);
if ( 1 === $above_midpoint_count ) {
$x_large_count = 2;
}
if ( $above_midpoint_count > 1 ) {
++$x_large_count;
}
$slug += 10;
}
$spacing_sizes = $below_sizes;
foreach ( $above_sizes as $above_sizes_item ) {
$spacing_sizes[] = $above_sizes_item;
}
return $spacing_sizes;
}
/**
* This is used to convert the internal representation of variables to the CSS representation.
* For example, `var:preset|color|vivid-green-cyan` becomes `var(--wp--preset--color--vivid-green-cyan)`.
*
* @since 6.3.0
*
* @param string $value The variable such as var:preset|color|vivid-green-cyan to convert.
* @return string The converted variable.
*/
private static function convert_custom_properties( $value ) {
$prefix = 'var:';
$prefix_len = strlen( $prefix );
$token_in = '|';
$token_out = '--';
if ( str_starts_with( $value, $prefix ) ) {
$unwrapped_name = str_replace(
$token_in,
$token_out,
substr( $value, $prefix_len )
);
$value = "var(--wp--$unwrapped_name)";
}
return $value;
}
/**
* Given a tree, converts the internal representation of variables to the CSS representation.
* It is recursive and modifies the input in-place.
*
* @since 6.3.0
*
* @param array $tree Input to process.
* @return array The modified $tree.
*/
private static function resolve_custom_css_format( $tree ) {
$prefix = 'var:';
foreach ( $tree as $key => $data ) {
if ( is_string( $data ) && str_starts_with( $data, $prefix ) ) {
$tree[ $key ] = self::convert_custom_properties( $data );
} elseif ( is_array( $data ) ) {
$tree[ $key ] = self::resolve_custom_css_format( $data );
}
}
return $tree;
}
/**
* Returns the selectors metadata for a block.
*
* @since 6.3.0
*
* @param object $block_type The block type.
* @param string $root_selector The block's root selector.
* @return array The custom selectors set by the block.
*/
protected static function get_block_selectors( $block_type, $root_selector ) {
if ( ! empty( $block_type->selectors ) ) {
return $block_type->selectors;
}
$selectors = array( 'root' => $root_selector );
foreach ( static::BLOCK_SUPPORT_FEATURE_LEVEL_SELECTORS as $key => $feature ) {
$feature_selector = wp_get_block_css_selector( $block_type, $key );
if ( null !== $feature_selector ) {
$selectors[ $feature ] = array( 'root' => $feature_selector );
}
}
return $selectors;
}
/**
* Generates all the element selectors for a block.
*
* @since 6.3.0
*
* @param string $root_selector The block's root CSS selector.
* @return array The block's element selectors.
*/
protected static function get_block_element_selectors( $root_selector ) {
/*
* Assign defaults, then override those that the block sets by itself.
* If the block selector is compounded, will append the element to each
* individual block selector.
*/
$block_selectors = explode( ',', $root_selector );
$element_selectors = array();
foreach ( static::ELEMENTS as $el_name => $el_selector ) {
$element_selector = array();
foreach ( $block_selectors as $selector ) {
if ( $selector === $el_selector ) {
$element_selector = array( $el_selector );
break;
}
$element_selector[] = static::prepend_to_selector( $el_selector, $selector . ' ' );
}
$element_selectors[ $el_name ] = implode( ',', $element_selector );
}
return $element_selectors;
}
/**
* Generates style declarations for a node's features e.g., color, border,
* typography etc. that have custom selectors in their related block's
* metadata.
*
* @since 6.3.0
*
* @param object $metadata The related block metadata containing selectors.
* @param object $node A merged theme.json node for block or variation.
* @return array The style declarations for the node's features with custom
* selectors.
*/
protected function get_feature_declarations_for_node( $metadata, &$node ) {
$declarations = array();
if ( ! isset( $metadata['selectors'] ) ) {
return $declarations;
}
$settings = isset( $this->theme_json['settings'] )
? $this->theme_json['settings']
: array();
foreach ( $metadata['selectors'] as $feature => $feature_selectors ) {
/*
* Skip if this is the block's root selector or the block doesn't
* have any styles for the feature.
*/
if ( 'root' === $feature || empty( $node[ $feature ] ) ) {
continue;
}
if ( is_array( $feature_selectors ) ) {
foreach ( $feature_selectors as $subfeature => $subfeature_selector ) {
if ( 'root' === $subfeature || empty( $node[ $feature ][ $subfeature ] ) ) {
continue;
}
/*
* Create temporary node containing only the subfeature data
* to leverage existing `compute_style_properties` function.
*/
$subfeature_node = array(
$feature => array(
$subfeature => $node[ $feature ][ $subfeature ],
),
);
// Generate style declarations.
$new_declarations = static::compute_style_properties( $subfeature_node, $settings, null, $this->theme_json );
// Merge subfeature declarations into feature declarations.
if ( isset( $declarations[ $subfeature_selector ] ) ) {
foreach ( $new_declarations as $new_declaration ) {
$declarations[ $subfeature_selector ][] = $new_declaration;
}
} else {
$declarations[ $subfeature_selector ] = $new_declarations;
}
/*
* Remove the subfeature from the block's node now its
* styles will be included under its own selector not the
* block's.
*/
unset( $node[ $feature ][ $subfeature ] );
}
}
/*
* Now subfeatures have been processed and removed we can process
* feature root selector or simple string selector.
*/
if (
is_string( $feature_selectors ) ||
( isset( $feature_selectors['root'] ) && $feature_selectors['root'] )
) {
$feature_selector = is_string( $feature_selectors ) ? $feature_selectors : $feature_selectors['root'];
/*
* Create temporary node containing only the feature data
* to leverage existing `compute_style_properties` function.
*/
$feature_node = array( $feature => $node[ $feature ] );
// Generate the style declarations.
$new_declarations = static::compute_style_properties( $feature_node, $settings, null, $this->theme_json );
/*
* Merge new declarations with any that already exist for
* the feature selector. This may occur when multiple block
* support features use the same custom selector.
*/
if ( isset( $declarations[ $feature_selector ] ) ) {
foreach ( $new_declarations as $new_declaration ) {
$declarations[ $feature_selector ][] = $new_declaration;
}
} else {
$declarations[ $feature_selector ] = $new_declarations;
}
/*
* Remove the feature from the block's node now its styles
* will be included under its own selector not the block's.
*/
unset( $node[ $feature ] );
}
}
return $declarations;
}
/**
* Replaces CSS variables with their values in place.
*
* @since 6.3.0
* @since 6.5.0 Check for empty style before processing its value.
*
* @param array $styles CSS declarations to convert.
* @param array $values key => value pairs to use for replacement.
* @return array
*/
private static function convert_variables_to_value( $styles, $values ) {
foreach ( $styles as $key => $style ) {
if ( empty( $style ) ) {
continue;
}
if ( is_array( $style ) ) {
$styles[ $key ] = self::convert_variables_to_value( $style, $values );
continue;
}
if ( 0 <= strpos( $style, 'var(' ) ) {
// find all the variables in the string in the form of var(--variable-name, fallback), with fallback in the second capture group.
$has_matches = preg_match_all( '/var\(([^),]+)?,?\s?(\S+)?\)/', $style, $var_parts );
if ( $has_matches ) {
$resolved_style = $styles[ $key ];
foreach ( $var_parts[1] as $index => $var_part ) {
$key_in_values = 'var(' . $var_part . ')';
$rule_to_replace = $var_parts[0][ $index ]; // the css rule to replace e.g. var(--wp--preset--color--vivid-green-cyan).
$fallback = $var_parts[2][ $index ]; // the fallback value.
$resolved_style = str_replace(
array(
$rule_to_replace,
$fallback,
),
array(
isset( $values[ $key_in_values ] ) ? $values[ $key_in_values ] : $rule_to_replace,
isset( $values[ $fallback ] ) ? $values[ $fallback ] : $fallback,
),
$resolved_style
);
}
$styles[ $key ] = $resolved_style;
}
}
}
return $styles;
}
/**
* Resolves the values of CSS variables in the given styles.
*
* @since 6.3.0
*
* @param WP_Theme_JSON $theme_json The theme json resolver.
* @return WP_Theme_JSON The $theme_json with resolved variables.
*/
public static function resolve_variables( $theme_json ) {
$settings = $theme_json->get_settings();
$styles = $theme_json->get_raw_data()['styles'];
$preset_vars = static::compute_preset_vars( $settings, static::VALID_ORIGINS );
$theme_vars = static::compute_theme_vars( $settings );
$vars = array_reduce(
array_merge( $preset_vars, $theme_vars ),
function ( $carry, $item ) {
$name = $item['name'];
$carry[ "var({$name})" ] = $item['value'];
return $carry;
},
array()
);
$theme_json->theme_json['styles'] = self::convert_variables_to_value( $styles, $vars );
return $theme_json;
}
/**
* Generates a selector for a block style variation.
*
* @since 6.5.0
*
* @param string $variation_name Name of the block style variation.
* @param string $block_selector CSS selector for the block.
* @return string Block selector with block style variation selector added to it.
*/
protected static function get_block_style_variation_selector( $variation_name, $block_selector ) {
$variation_class = ".is-style-$variation_name";
if ( ! $block_selector ) {
return $variation_class;
}
$limit = 1;
$selector_parts = explode( ',', $block_selector );
$result = array();
foreach ( $selector_parts as $part ) {
$result[] = preg_replace_callback(
'/((?::\([^)]+\))?\s*)([^\s:]+)/',
function ( $matches ) use ( $variation_class ) {
return $matches[1] . $matches[2] . $variation_class;
},
$part,
$limit
);
}
return implode( ',', $result );
}
/**
* Collects valid block style variations keyed by block type.
*
* @since 6.6.0
* @since 6.8.0 Added the `$blocks_metadata` parameter.
*
* @param array $blocks_metadata Optional. List of metadata per block. Default is the metadata for all blocks.
* @return array Valid block style variations by block type.
*/
protected static function get_valid_block_style_variations( $blocks_metadata = array() ) {
$valid_variations = array();
$blocks_metadata = empty( $blocks_metadata ) ? static::get_blocks_metadata() : $blocks_metadata;
foreach ( $blocks_metadata as $block_name => $block_meta ) {
if ( ! isset( $block_meta['styleVariations'] ) ) {
continue;
}
$valid_variations[ $block_name ] = array_keys( $block_meta['styleVariations'] );
}
return $valid_variations;
}
}
array(
'name_admin_bar' => _x( 'Post', 'add new from admin bar' ),
),
'public' => true,
'_builtin' => true, /* internal use only. don't use this when registering your own post type. */
'_edit_link' => 'post.php?post=%d', /* internal use only. don't use this when registering your own post type. */
'capability_type' => 'post',
'map_meta_cap' => true,
'menu_position' => 5,
'menu_icon' => 'dashicons-admin-post',
'hierarchical' => false,
'rewrite' => false,
'query_var' => false,
'delete_with_user' => true,
'supports' => array(
'title',
'editor' => array( 'notes' => true ),
'author',
'thumbnail',
'excerpt',
'trackbacks',
'custom-fields',
'comments',
'revisions',
'post-formats',
),
'show_in_rest' => true,
'rest_base' => 'posts',
'rest_controller_class' => 'WP_REST_Posts_Controller',
)
);
register_post_type(
'page',
array(
'labels' => array(
'name_admin_bar' => _x( 'Page', 'add new from admin bar' ),
),
'public' => true,
'publicly_queryable' => false,
'_builtin' => true, /* internal use only. don't use this when registering your own post type. */
'_edit_link' => 'post.php?post=%d', /* internal use only. don't use this when registering your own post type. */
'capability_type' => 'page',
'map_meta_cap' => true,
'menu_position' => 20,
'menu_icon' => 'dashicons-admin-page',
'hierarchical' => true,
'rewrite' => false,
'query_var' => false,
'delete_with_user' => true,
'supports' => array(
'title',
'editor' => array( 'notes' => true ),
'author',
'thumbnail',
'page-attributes',
'custom-fields',
'comments',
'revisions',
),
'show_in_rest' => true,
'rest_base' => 'pages',
'rest_controller_class' => 'WP_REST_Posts_Controller',
)
);
register_post_type(
'attachment',
array(
'labels' => array(
'name' => _x( 'Media', 'post type general name' ),
'name_admin_bar' => _x( 'Media', 'add new from admin bar' ),
'add_new' => __( 'Add Media File' ),
'add_new_item' => __( 'Add Media File' ),
'edit_item' => __( 'Edit Media' ),
'view_item' => ( '1' === get_option( 'wp_attachment_pages_enabled' ) ) ? __( 'View Attachment Page' ) : __( 'View Media File' ),
'attributes' => __( 'Attachment Attributes' ),
),
'public' => true,
'show_ui' => true,
'_builtin' => true, /* internal use only. don't use this when registering your own post type. */
'_edit_link' => 'post.php?post=%d', /* internal use only. don't use this when registering your own post type. */
'capability_type' => 'post',
'capabilities' => array(
'create_posts' => 'upload_files',
),
'map_meta_cap' => true,
'menu_icon' => 'dashicons-admin-media',
'hierarchical' => false,
'rewrite' => false,
'query_var' => false,
'show_in_nav_menus' => false,
'delete_with_user' => true,
'supports' => array( 'title', 'author', 'comments' ),
'show_in_rest' => true,
'rest_base' => 'media',
'rest_controller_class' => 'WP_REST_Attachments_Controller',
)
);
add_post_type_support( 'attachment:audio', 'thumbnail' );
add_post_type_support( 'attachment:video', 'thumbnail' );
register_post_type(
'revision',
array(
'labels' => array(
'name' => __( 'Revisions' ),
'singular_name' => __( 'Revision' ),
),
'public' => false,
'_builtin' => true, /* internal use only. don't use this when registering your own post type. */
'_edit_link' => 'revision.php?revision=%d', /* internal use only. don't use this when registering your own post type. */
'capability_type' => 'post',
'map_meta_cap' => true,
'hierarchical' => false,
'rewrite' => false,
'query_var' => false,
'can_export' => false,
'delete_with_user' => true,
'supports' => array( 'author' ),
)
);
register_post_type(
'nav_menu_item',
array(
'labels' => array(
'name' => __( 'Navigation Menu Items' ),
'singular_name' => __( 'Navigation Menu Item' ),
),
'public' => false,
'_builtin' => true, /* internal use only. don't use this when registering your own post type. */
'hierarchical' => false,
'rewrite' => false,
'delete_with_user' => false,
'query_var' => false,
'map_meta_cap' => true,
'capability_type' => array( 'edit_theme_options', 'edit_theme_options' ),
'capabilities' => array(
// Meta Capabilities.
'edit_post' => 'edit_post',
'read_post' => 'read_post',
'delete_post' => 'delete_post',
// Primitive Capabilities.
'edit_posts' => 'edit_theme_options',
'edit_others_posts' => 'edit_theme_options',
'delete_posts' => 'edit_theme_options',
'publish_posts' => 'edit_theme_options',
'read_private_posts' => 'edit_theme_options',
'read' => 'read',
'delete_private_posts' => 'edit_theme_options',
'delete_published_posts' => 'edit_theme_options',
'delete_others_posts' => 'edit_theme_options',
'edit_private_posts' => 'edit_theme_options',
'edit_published_posts' => 'edit_theme_options',
),
'show_in_rest' => true,
'rest_base' => 'menu-items',
'rest_controller_class' => 'WP_REST_Menu_Items_Controller',
)
);
register_post_type(
'custom_css',
array(
'labels' => array(
'name' => __( 'Custom CSS' ),
'singular_name' => __( 'Custom CSS' ),
),
'public' => false,
'hierarchical' => false,
'rewrite' => false,
'query_var' => false,
'delete_with_user' => false,
'can_export' => true,
'_builtin' => true, /* internal use only. don't use this when registering your own post type. */
'supports' => array( 'title', 'revisions' ),
'capabilities' => array(
'delete_posts' => 'edit_theme_options',
'delete_post' => 'edit_theme_options',
'delete_published_posts' => 'edit_theme_options',
'delete_private_posts' => 'edit_theme_options',
'delete_others_posts' => 'edit_theme_options',
'edit_post' => 'edit_css',
'edit_posts' => 'edit_css',
'edit_others_posts' => 'edit_css',
'edit_published_posts' => 'edit_css',
'read_post' => 'read',
'read_private_posts' => 'read',
'publish_posts' => 'edit_theme_options',
),
)
);
register_post_type(
'customize_changeset',
array(
'labels' => array(
'name' => _x( 'Changesets', 'post type general name' ),
'singular_name' => _x( 'Changeset', 'post type singular name' ),
'add_new' => __( 'Add Changeset' ),
'add_new_item' => __( 'Add Changeset' ),
'new_item' => __( 'New Changeset' ),
'edit_item' => __( 'Edit Changeset' ),
'view_item' => __( 'View Changeset' ),
'all_items' => __( 'All Changesets' ),
'search_items' => __( 'Search Changesets' ),
'not_found' => __( 'No changesets found.' ),
'not_found_in_trash' => __( 'No changesets found in Trash.' ),
),
'public' => false,
'_builtin' => true, /* internal use only. don't use this when registering your own post type. */
'map_meta_cap' => true,
'hierarchical' => false,
'rewrite' => false,
'query_var' => false,
'can_export' => false,
'delete_with_user' => false,
'supports' => array( 'title', 'author' ),
'capability_type' => 'customize_changeset',
'capabilities' => array(
'create_posts' => 'customize',
'delete_others_posts' => 'customize',
'delete_post' => 'customize',
'delete_posts' => 'customize',
'delete_private_posts' => 'customize',
'delete_published_posts' => 'customize',
'edit_others_posts' => 'customize',
'edit_post' => 'customize',
'edit_posts' => 'customize',
'edit_private_posts' => 'customize',
'edit_published_posts' => 'do_not_allow',
'publish_posts' => 'customize',
'read' => 'read',
'read_post' => 'customize',
'read_private_posts' => 'customize',
),
)
);
register_post_type(
'oembed_cache',
array(
'labels' => array(
'name' => __( 'oEmbed Responses' ),
'singular_name' => __( 'oEmbed Response' ),
),
'public' => false,
'hierarchical' => false,
'rewrite' => false,
'query_var' => false,
'delete_with_user' => false,
'can_export' => false,
'_builtin' => true, /* internal use only. don't use this when registering your own post type. */
'supports' => array(),
)
);
register_post_type(
'user_request',
array(
'labels' => array(
'name' => __( 'User Requests' ),
'singular_name' => __( 'User Request' ),
),
'public' => false,
'_builtin' => true, /* internal use only. don't use this when registering your own post type. */
'hierarchical' => false,
'rewrite' => false,
'query_var' => false,
'can_export' => false,
'delete_with_user' => false,
'supports' => array(),
)
);
register_post_type(
'wp_block',
array(
'labels' => array(
'name' => _x( 'Patterns', 'post type general name' ),
'singular_name' => _x( 'Pattern', 'post type singular name' ),
'add_new' => __( 'Add Pattern' ),
'add_new_item' => __( 'Add Pattern' ),
'new_item' => __( 'New Pattern' ),
'edit_item' => __( 'Edit Block Pattern' ),
'view_item' => __( 'View Pattern' ),
'view_items' => __( 'View Patterns' ),
'all_items' => __( 'All Patterns' ),
'search_items' => __( 'Search Patterns' ),
'not_found' => __( 'No patterns found.' ),
'not_found_in_trash' => __( 'No patterns found in Trash.' ),
'filter_items_list' => __( 'Filter patterns list' ),
'items_list_navigation' => __( 'Patterns list navigation' ),
'items_list' => __( 'Patterns list' ),
'item_published' => __( 'Pattern published.' ),
'item_published_privately' => __( 'Pattern published privately.' ),
'item_reverted_to_draft' => __( 'Pattern reverted to draft.' ),
'item_scheduled' => __( 'Pattern scheduled.' ),
'item_updated' => __( 'Pattern updated.' ),
),
'public' => false,
'_builtin' => true, /* internal use only. don't use this when registering your own post type. */
'show_ui' => true,
'show_in_menu' => false,
'rewrite' => false,
'show_in_rest' => true,
'rest_base' => 'blocks',
'rest_controller_class' => 'WP_REST_Blocks_Controller',
'capability_type' => 'block',
'capabilities' => array(
// You need to be able to edit posts, in order to read blocks in their raw form.
'read' => 'edit_posts',
// You need to be able to publish posts, in order to create blocks.
'create_posts' => 'publish_posts',
'edit_posts' => 'edit_posts',
'edit_published_posts' => 'edit_published_posts',
'delete_published_posts' => 'delete_published_posts',
// Enables trashing draft posts as well.
'delete_posts' => 'delete_posts',
'edit_others_posts' => 'edit_others_posts',
'delete_others_posts' => 'delete_others_posts',
),
'map_meta_cap' => true,
'supports' => array(
'title',
'excerpt',
'editor',
'revisions',
'custom-fields',
),
)
);
$template_edit_link = 'site-editor.php?' . build_query(
array(
'postType' => '%s',
'postId' => '%s',
'canvas' => 'edit',
)
);
register_post_type(
'wp_template',
array(
'labels' => array(
'name' => _x( 'Templates', 'post type general name' ),
'singular_name' => _x( 'Template', 'post type singular name' ),
'add_new' => __( 'Add Template' ),
'add_new_item' => __( 'Add Template' ),
'new_item' => __( 'New Template' ),
'edit_item' => __( 'Edit Template' ),
'view_item' => __( 'View Template' ),
'all_items' => __( 'Templates' ),
'search_items' => __( 'Search Templates' ),
'parent_item_colon' => __( 'Parent Template:' ),
'not_found' => __( 'No templates found.' ),
'not_found_in_trash' => __( 'No templates found in Trash.' ),
'archives' => __( 'Template archives' ),
'insert_into_item' => __( 'Insert into template' ),
'uploaded_to_this_item' => __( 'Uploaded to this template' ),
'filter_items_list' => __( 'Filter templates list' ),
'items_list_navigation' => __( 'Templates list navigation' ),
'items_list' => __( 'Templates list' ),
'item_updated' => __( 'Template updated.' ),
),
'description' => __( 'Templates to include in your theme.' ),
'public' => false,
'_builtin' => true, /* internal use only. don't use this when registering your own post type. */
'_edit_link' => $template_edit_link, /* internal use only. don't use this when registering your own post type. */
'has_archive' => false,
'show_ui' => false,
'show_in_menu' => false,
'show_in_rest' => true,
'rewrite' => false,
'rest_base' => 'templates',
'rest_controller_class' => 'WP_REST_Templates_Controller',
'autosave_rest_controller_class' => 'WP_REST_Template_Autosaves_Controller',
'revisions_rest_controller_class' => 'WP_REST_Template_Revisions_Controller',
'late_route_registration' => true,
'capability_type' => array( 'template', 'templates' ),
'capabilities' => array(
'create_posts' => 'edit_theme_options',
'delete_posts' => 'edit_theme_options',
'delete_others_posts' => 'edit_theme_options',
'delete_private_posts' => 'edit_theme_options',
'delete_published_posts' => 'edit_theme_options',
'edit_posts' => 'edit_theme_options',
'edit_others_posts' => 'edit_theme_options',
'edit_private_posts' => 'edit_theme_options',
'edit_published_posts' => 'edit_theme_options',
'publish_posts' => 'edit_theme_options',
'read' => 'edit_theme_options',
'read_private_posts' => 'edit_theme_options',
),
'map_meta_cap' => true,
'supports' => array(
'title',
'slug',
'excerpt',
'editor',
'revisions',
'author',
),
)
);
register_post_type(
'wp_template_part',
array(
'labels' => array(
'name' => _x( 'Template Parts', 'post type general name' ),
'singular_name' => _x( 'Template Part', 'post type singular name' ),
'add_new' => __( 'Add Template Part' ),
'add_new_item' => __( 'Add Template Part' ),
'new_item' => __( 'New Template Part' ),
'edit_item' => __( 'Edit Template Part' ),
'view_item' => __( 'View Template Part' ),
'all_items' => __( 'Template Parts' ),
'search_items' => __( 'Search Template Parts' ),
'parent_item_colon' => __( 'Parent Template Part:' ),
'not_found' => __( 'No template parts found.' ),
'not_found_in_trash' => __( 'No template parts found in Trash.' ),
'archives' => __( 'Template part archives' ),
'insert_into_item' => __( 'Insert into template part' ),
'uploaded_to_this_item' => __( 'Uploaded to this template part' ),
'filter_items_list' => __( 'Filter template parts list' ),
'items_list_navigation' => __( 'Template parts list navigation' ),
'items_list' => __( 'Template parts list' ),
'item_updated' => __( 'Template part updated.' ),
),
'description' => __( 'Template parts to include in your templates.' ),
'public' => false,
'_builtin' => true, /* internal use only. don't use this when registering your own post type. */
'_edit_link' => $template_edit_link, /* internal use only. don't use this when registering your own post type. */
'has_archive' => false,
'show_ui' => false,
'show_in_menu' => false,
'show_in_rest' => true,
'rewrite' => false,
'rest_base' => 'template-parts',
'rest_controller_class' => 'WP_REST_Templates_Controller',
'autosave_rest_controller_class' => 'WP_REST_Template_Autosaves_Controller',
'revisions_rest_controller_class' => 'WP_REST_Template_Revisions_Controller',
'late_route_registration' => true,
'map_meta_cap' => true,
'capabilities' => array(
'create_posts' => 'edit_theme_options',
'delete_posts' => 'edit_theme_options',
'delete_others_posts' => 'edit_theme_options',
'delete_private_posts' => 'edit_theme_options',
'delete_published_posts' => 'edit_theme_options',
'edit_posts' => 'edit_theme_options',
'edit_others_posts' => 'edit_theme_options',
'edit_private_posts' => 'edit_theme_options',
'edit_published_posts' => 'edit_theme_options',
'publish_posts' => 'edit_theme_options',
'read' => 'edit_theme_options',
'read_private_posts' => 'edit_theme_options',
),
'supports' => array(
'title',
'slug',
'excerpt',
'editor',
'revisions',
'author',
),
)
);
register_post_type(
'wp_global_styles',
array(
'label' => _x( 'Global Styles', 'post type general name' ),
'description' => __( 'Global styles to include in themes.' ),
'public' => false,
'_builtin' => true, /* internal use only. don't use this when registering your own post type. */
'_edit_link' => '/site-editor.php?canvas=edit', /* internal use only. don't use this when registering your own post type. */
'show_ui' => false,
'show_in_rest' => true,
'rewrite' => false,
'rest_base' => 'global-styles',
'rest_controller_class' => 'WP_REST_Global_Styles_Controller',
'revisions_rest_controller_class' => 'WP_REST_Global_Styles_Revisions_Controller',
'late_route_registration' => true,
'capabilities' => array(
'read' => 'edit_posts',
'create_posts' => 'edit_theme_options',
'edit_posts' => 'edit_theme_options',
'edit_published_posts' => 'edit_theme_options',
'delete_published_posts' => 'edit_theme_options',
'edit_others_posts' => 'edit_theme_options',
'delete_others_posts' => 'edit_theme_options',
),
'map_meta_cap' => true,
'supports' => array(
'title',
'editor',
'revisions',
),
)
);
// Disable autosave endpoints for global styles.
remove_post_type_support( 'wp_global_styles', 'autosave' );
$navigation_post_edit_link = 'site-editor.php?' . build_query(
array(
'postId' => '%s',
'postType' => 'wp_navigation',
'canvas' => 'edit',
)
);
register_post_type(
'wp_navigation',
array(
'labels' => array(
'name' => _x( 'Navigation Menus', 'post type general name' ),
'singular_name' => _x( 'Navigation Menu', 'post type singular name' ),
'add_new' => __( 'Add Navigation Menu' ),
'add_new_item' => __( 'Add Navigation Menu' ),
'new_item' => __( 'New Navigation Menu' ),
'edit_item' => __( 'Edit Navigation Menu' ),
'view_item' => __( 'View Navigation Menu' ),
'all_items' => __( 'Navigation Menus' ),
'search_items' => __( 'Search Navigation Menus' ),
'parent_item_colon' => __( 'Parent Navigation Menu:' ),
'not_found' => __( 'No Navigation Menu found.' ),
'not_found_in_trash' => __( 'No Navigation Menu found in Trash.' ),
'archives' => __( 'Navigation Menu archives' ),
'insert_into_item' => __( 'Insert into Navigation Menu' ),
'uploaded_to_this_item' => __( 'Uploaded to this Navigation Menu' ),
'filter_items_list' => __( 'Filter Navigation Menu list' ),
'items_list_navigation' => __( 'Navigation Menus list navigation' ),
'items_list' => __( 'Navigation Menus list' ),
),
'description' => __( 'Navigation menus that can be inserted into your site.' ),
'public' => false,
'_builtin' => true, /* internal use only. don't use this when registering your own post type. */
'_edit_link' => $navigation_post_edit_link, /* internal use only. don't use this when registering your own post type. */
'has_archive' => false,
'show_ui' => true,
'show_in_menu' => false,
'show_in_admin_bar' => false,
'show_in_rest' => true,
'rewrite' => false,
'map_meta_cap' => true,
'capabilities' => array(
'edit_others_posts' => 'edit_theme_options',
'delete_posts' => 'edit_theme_options',
'publish_posts' => 'edit_theme_options',
'create_posts' => 'edit_theme_options',
'read_private_posts' => 'edit_theme_options',
'delete_private_posts' => 'edit_theme_options',
'delete_published_posts' => 'edit_theme_options',
'delete_others_posts' => 'edit_theme_options',
'edit_private_posts' => 'edit_theme_options',
'edit_published_posts' => 'edit_theme_options',
'edit_posts' => 'edit_theme_options',
),
'rest_base' => 'navigation',
'rest_controller_class' => 'WP_REST_Posts_Controller',
'supports' => array(
'title',
'editor',
'revisions',
),
)
);
register_post_type(
'wp_font_family',
array(
'labels' => array(
'name' => __( 'Font Families' ),
'singular_name' => __( 'Font Family' ),
),
'public' => false,
'_builtin' => true, /* internal use only. don't use this when registering your own post type. */
'hierarchical' => false,
'capabilities' => array(
'read' => 'edit_theme_options',
'read_private_posts' => 'edit_theme_options',
'create_posts' => 'edit_theme_options',
'publish_posts' => 'edit_theme_options',
'edit_posts' => 'edit_theme_options',
'edit_others_posts' => 'edit_theme_options',
'edit_published_posts' => 'edit_theme_options',
'delete_posts' => 'edit_theme_options',
'delete_others_posts' => 'edit_theme_options',
'delete_published_posts' => 'edit_theme_options',
),
'map_meta_cap' => true,
'query_var' => false,
'rewrite' => false,
'show_in_rest' => true,
'rest_base' => 'font-families',
'rest_controller_class' => 'WP_REST_Font_Families_Controller',
'supports' => array( 'title' ),
)
);
register_post_type(
'wp_font_face',
array(
'labels' => array(
'name' => __( 'Font Faces' ),
'singular_name' => __( 'Font Face' ),
),
'public' => false,
'_builtin' => true, /* internal use only. don't use this when registering your own post type. */
'hierarchical' => false,
'capabilities' => array(
'read' => 'edit_theme_options',
'read_private_posts' => 'edit_theme_options',
'create_posts' => 'edit_theme_options',
'publish_posts' => 'edit_theme_options',
'edit_posts' => 'edit_theme_options',
'edit_others_posts' => 'edit_theme_options',
'edit_published_posts' => 'edit_theme_options',
'delete_posts' => 'edit_theme_options',
'delete_others_posts' => 'edit_theme_options',
'delete_published_posts' => 'edit_theme_options',
),
'map_meta_cap' => true,
'query_var' => false,
'rewrite' => false,
'show_in_rest' => true,
'rest_base' => 'font-families/(?PVHOST',
'SUBDOMAIN_INSTALL',
'wp-config.php',
'is_subdomain_install()'
);
if ( $subdomain_error_warn ) {
wp_trigger_error(
__FUNCTION__,
sprintf(
/* translators: 1: VHOST, 2: SUBDOMAIN_INSTALL */
__( 'Conflicting values for the constants %1$s and %2$s. The value of %2$s will be assumed to be your subdomain configuration setting.' ),
'VHOST',
'SUBDOMAIN_INSTALL'
) . ' ' . $vhost_deprecated,
E_USER_WARNING
);
} else {
_deprecated_argument( 'define()', '3.0.0', $vhost_deprecated );
}
return;
}
if ( defined( 'SUBDOMAIN_INSTALL' ) && defined( 'VHOST' ) ) {
$subdomain_error = true;
if ( SUBDOMAIN_INSTALL !== ( 'yes' === VHOST ) ) {
$subdomain_error_warn = true;
}
} elseif ( defined( 'SUBDOMAIN_INSTALL' ) ) {
$subdomain_error = false;
define( 'VHOST', SUBDOMAIN_INSTALL ? 'yes' : 'no' );
} elseif ( defined( 'VHOST' ) ) {
$subdomain_error = true;
define( 'SUBDOMAIN_INSTALL', 'yes' === VHOST );
} else {
$subdomain_error = false;
define( 'SUBDOMAIN_INSTALL', false );
define( 'VHOST', 'no' );
}
}
set_name( $store_name );
}
return static::$stores[ $store_name ];
}
/**
* Gets an array of all available stores.
*
* @since 6.1.0
*
* @return WP_Style_Engine_CSS_Rules_Store[]
*/
public static function get_stores() {
return static::$stores;
}
/**
* Clears all stores from static::$stores.
*
* @since 6.1.0
*/
public static function remove_all_stores() {
static::$stores = array();
}
/**
* Sets the store name.
*
* @since 6.1.0
*
* @param string $name The store name.
*/
public function set_name( $name ) {
$this->name = $name;
}
/**
* Gets the store name.
*
* @since 6.1.0
*
* @return string
*/
public function get_name() {
return $this->name;
}
/**
* Gets an array of all rules.
*
* @since 6.1.0
*
* @return WP_Style_Engine_CSS_Rule[]
*/
public function get_all_rules() {
return $this->rules;
}
/**
* Gets a WP_Style_Engine_CSS_Rule object by its selector.
* If the rule does not exist, it will be created.
*
* @since 6.1.0
* @since 6.6.0 Added the $rules_group parameter.
*
* @param string $selector The CSS selector.
* @param string $rules_group A parent CSS selector in the case of nested CSS, or a CSS nested @rule,
* such as `@media (min-width: 80rem)` or `@layer module`.
* @return WP_Style_Engine_CSS_Rule|void Returns a WP_Style_Engine_CSS_Rule object,
* or void if the selector is empty.
*/
public function add_rule( $selector, $rules_group = '' ) {
$selector = $selector ? trim( $selector ) : '';
$rules_group = $rules_group ? trim( $rules_group ) : '';
// Bail early if there is no selector.
if ( empty( $selector ) ) {
return;
}
if ( ! empty( $rules_group ) ) {
if ( empty( $this->rules[ "$rules_group $selector" ] ) ) {
$this->rules[ "$rules_group $selector" ] = new WP_Style_Engine_CSS_Rule( $selector, array(), $rules_group );
}
return $this->rules[ "$rules_group $selector" ];
}
// Create the rule if it doesn't exist.
if ( empty( $this->rules[ $selector ] ) ) {
$this->rules[ $selector ] = new WP_Style_Engine_CSS_Rule( $selector );
}
return $this->rules[ $selector ];
}
/**
* Removes a selector from the store.
*
* @since 6.1.0
*
* @param string $selector The CSS selector.
*/
public function remove_rule( $selector ) {
unset( $this->rules[ $selector ] );
}
}
(array) an array of classnames to be returned for block styles. The key is a classname or pattern.
* A value of `true` means the classname should be applied always. Otherwise, a valid CSS property (string)
* to match the incoming value, e.g., "color" to match var:preset|color|somePresetSlug.
* - css_vars => (array) an array of key value pairs used to generate CSS var values.
* The key should be the CSS property name that matches the second element of the preset string value,
* i.e., "color" in var:preset|color|somePresetSlug. The value is a CSS var pattern (e.g. `--wp--preset--color--$slug`),
* whose `$slug` fragment will be replaced with the preset slug, which is the third element of the preset string value,
* i.e., `somePresetSlug` in var:preset|color|somePresetSlug.
* - property_keys => (array) array of keys whose values represent a valid CSS property, e.g., "margin" or "border".
* - path => (array) a path that accesses the corresponding style value in the block style object.
* - value_func => (string) the name of a function to generate a CSS definition array for a particular style object. The output of this function should be `array( "$property" => "$value", ... )`.
*
* @since 6.1.0
* @var array
*/
const BLOCK_STYLE_DEFINITIONS_METADATA = array(
'background' => array(
'backgroundImage' => array(
'property_keys' => array(
'default' => 'background-image',
),
'value_func' => array( self::class, 'get_url_or_value_css_declaration' ),
'path' => array( 'background', 'backgroundImage' ),
),
'backgroundPosition' => array(
'property_keys' => array(
'default' => 'background-position',
),
'path' => array( 'background', 'backgroundPosition' ),
),
'backgroundRepeat' => array(
'property_keys' => array(
'default' => 'background-repeat',
),
'path' => array( 'background', 'backgroundRepeat' ),
),
'backgroundSize' => array(
'property_keys' => array(
'default' => 'background-size',
),
'path' => array( 'background', 'backgroundSize' ),
),
'backgroundAttachment' => array(
'property_keys' => array(
'default' => 'background-attachment',
),
'path' => array( 'background', 'backgroundAttachment' ),
),
),
'color' => array(
'text' => array(
'property_keys' => array(
'default' => 'color',
),
'path' => array( 'color', 'text' ),
'css_vars' => array(
'color' => '--wp--preset--color--$slug',
),
'classnames' => array(
'has-text-color' => true,
'has-$slug-color' => 'color',
),
),
'background' => array(
'property_keys' => array(
'default' => 'background-color',
),
'path' => array( 'color', 'background' ),
'css_vars' => array(
'color' => '--wp--preset--color--$slug',
),
'classnames' => array(
'has-background' => true,
'has-$slug-background-color' => 'color',
),
),
'gradient' => array(
'property_keys' => array(
'default' => 'background',
),
'path' => array( 'color', 'gradient' ),
'css_vars' => array(
'gradient' => '--wp--preset--gradient--$slug',
),
'classnames' => array(
'has-background' => true,
'has-$slug-gradient-background' => 'gradient',
),
),
),
'border' => array(
'color' => array(
'property_keys' => array(
'default' => 'border-color',
'individual' => 'border-%s-color',
),
'path' => array( 'border', 'color' ),
'classnames' => array(
'has-border-color' => true,
'has-$slug-border-color' => 'color',
),
),
'radius' => array(
'property_keys' => array(
'default' => 'border-radius',
'individual' => 'border-%s-radius',
),
'path' => array( 'border', 'radius' ),
'css_vars' => array(
'border-radius' => '--wp--preset--border-radius--$slug',
),
),
'style' => array(
'property_keys' => array(
'default' => 'border-style',
'individual' => 'border-%s-style',
),
'path' => array( 'border', 'style' ),
),
'width' => array(
'property_keys' => array(
'default' => 'border-width',
'individual' => 'border-%s-width',
),
'path' => array( 'border', 'width' ),
),
'top' => array(
'value_func' => array( self::class, 'get_individual_property_css_declarations' ),
'path' => array( 'border', 'top' ),
'css_vars' => array(
'color' => '--wp--preset--color--$slug',
),
),
'right' => array(
'value_func' => array( self::class, 'get_individual_property_css_declarations' ),
'path' => array( 'border', 'right' ),
'css_vars' => array(
'color' => '--wp--preset--color--$slug',
),
),
'bottom' => array(
'value_func' => array( self::class, 'get_individual_property_css_declarations' ),
'path' => array( 'border', 'bottom' ),
'css_vars' => array(
'color' => '--wp--preset--color--$slug',
),
),
'left' => array(
'value_func' => array( self::class, 'get_individual_property_css_declarations' ),
'path' => array( 'border', 'left' ),
'css_vars' => array(
'color' => '--wp--preset--color--$slug',
),
),
),
'shadow' => array(
'shadow' => array(
'property_keys' => array(
'default' => 'box-shadow',
),
'path' => array( 'shadow' ),
'css_vars' => array(
'shadow' => '--wp--preset--shadow--$slug',
),
),
),
'dimensions' => array(
'aspectRatio' => array(
'property_keys' => array(
'default' => 'aspect-ratio',
),
'path' => array( 'dimensions', 'aspectRatio' ),
'classnames' => array(
'has-aspect-ratio' => true,
),
),
'minHeight' => array(
'property_keys' => array(
'default' => 'min-height',
),
'path' => array( 'dimensions', 'minHeight' ),
'css_vars' => array(
'spacing' => '--wp--preset--spacing--$slug',
),
),
),
'spacing' => array(
'padding' => array(
'property_keys' => array(
'default' => 'padding',
'individual' => 'padding-%s',
),
'path' => array( 'spacing', 'padding' ),
'css_vars' => array(
'spacing' => '--wp--preset--spacing--$slug',
),
),
'margin' => array(
'property_keys' => array(
'default' => 'margin',
'individual' => 'margin-%s',
),
'path' => array( 'spacing', 'margin' ),
'css_vars' => array(
'spacing' => '--wp--preset--spacing--$slug',
),
),
),
'typography' => array(
'fontSize' => array(
'property_keys' => array(
'default' => 'font-size',
),
'path' => array( 'typography', 'fontSize' ),
'css_vars' => array(
'font-size' => '--wp--preset--font-size--$slug',
),
'classnames' => array(
'has-$slug-font-size' => 'font-size',
),
),
'fontFamily' => array(
'property_keys' => array(
'default' => 'font-family',
),
'css_vars' => array(
'font-family' => '--wp--preset--font-family--$slug',
),
'path' => array( 'typography', 'fontFamily' ),
'classnames' => array(
'has-$slug-font-family' => 'font-family',
),
),
'fontStyle' => array(
'property_keys' => array(
'default' => 'font-style',
),
'path' => array( 'typography', 'fontStyle' ),
),
'fontWeight' => array(
'property_keys' => array(
'default' => 'font-weight',
),
'path' => array( 'typography', 'fontWeight' ),
),
'lineHeight' => array(
'property_keys' => array(
'default' => 'line-height',
),
'path' => array( 'typography', 'lineHeight' ),
),
'textColumns' => array(
'property_keys' => array(
'default' => 'column-count',
),
'path' => array( 'typography', 'textColumns' ),
),
'textDecoration' => array(
'property_keys' => array(
'default' => 'text-decoration',
),
'path' => array( 'typography', 'textDecoration' ),
),
'textTransform' => array(
'property_keys' => array(
'default' => 'text-transform',
),
'path' => array( 'typography', 'textTransform' ),
),
'letterSpacing' => array(
'property_keys' => array(
'default' => 'letter-spacing',
),
'path' => array( 'typography', 'letterSpacing' ),
),
'writingMode' => array(
'property_keys' => array(
'default' => 'writing-mode',
),
'path' => array( 'typography', 'writingMode' ),
),
),
);
/**
* Util: Extracts the slug in kebab case from a preset string,
* e.g. `heavenly-blue` from `var:preset|color|heavenlyBlue`.
*
* @since 6.1.0
*
* @param string $style_value A single CSS preset value.
* @param string $property_key The CSS property that is the second element of the preset string.
* Used for matching.
* @return string The slug, or empty string if not found.
*/
protected static function get_slug_from_preset_value( $style_value, $property_key ) {
if ( is_string( $style_value ) && is_string( $property_key )
&& str_contains( $style_value, "var:preset|{$property_key}|" )
) {
$index_to_splice = strrpos( $style_value, '|' ) + 1;
return _wp_to_kebab_case( substr( $style_value, $index_to_splice ) );
}
return '';
}
/**
* Util: Generates a CSS var string, e.g. `var(--wp--preset--color--background)`
* from a preset string such as `var:preset|space|50`.
*
* @since 6.1.0
*
* @param string $style_value A single CSS preset value.
* @param string[] $css_vars An associate array of CSS var patterns
* used to generate the var string.
* @return string The CSS var, or an empty string if no match for slug found.
*/
protected static function get_css_var_value( $style_value, $css_vars ) {
foreach ( $css_vars as $property_key => $css_var_pattern ) {
$slug = static::get_slug_from_preset_value( $style_value, $property_key );
if ( static::is_valid_style_value( $slug ) ) {
$var = strtr(
$css_var_pattern,
array( '$slug' => $slug )
);
return "var($var)";
}
}
return '';
}
/**
* Util: Checks whether an incoming block style value is valid.
*
* @since 6.1.0
*
* @param string $style_value A single CSS preset value.
* @return bool
*/
protected static function is_valid_style_value( $style_value ) {
return '0' === $style_value || ! empty( $style_value );
}
/**
* Stores a CSS rule using the provided CSS selector and CSS declarations.
*
* @since 6.1.0
* @since 6.6.0 Added the `$rules_group` parameter.
*
* @param string $store_name A valid store key.
* @param string $css_selector When a selector is passed, the function will return
* a full CSS rule `$selector { ...rules }`
* otherwise a concatenated string of properties and values.
* @param string[] $css_declarations An associative array of CSS definitions,
* e.g. `array( "$property" => "$value", "$property" => "$value" )`.
* @param string $rules_group Optional. A parent CSS selector in the case of nested CSS, or a CSS nested @rule,
* such as `@media (min-width: 80rem)` or `@layer module`.
*/
public static function store_css_rule( $store_name, $css_selector, $css_declarations, $rules_group = '' ) {
if ( empty( $store_name ) || empty( $css_selector ) || empty( $css_declarations ) ) {
return;
}
static::get_store( $store_name )->add_rule( $css_selector, $rules_group )->add_declarations( $css_declarations );
}
/**
* Returns a store by store key.
*
* @since 6.1.0
*
* @param string $store_name A store key.
* @return WP_Style_Engine_CSS_Rules_Store|null
*/
public static function get_store( $store_name ) {
return WP_Style_Engine_CSS_Rules_Store::get_store( $store_name );
}
/**
* Returns classnames and CSS based on the values in a styles object.
*
* Return values are parsed based on the instructions in BLOCK_STYLE_DEFINITIONS_METADATA.
*
* @since 6.1.0
*
* @param array $block_styles The style object.
* @param array $options {
* Optional. An array of options. Default empty array.
*
* @type bool $convert_vars_to_classnames Whether to skip converting incoming CSS var patterns,
* e.g. `var:preset|id',
$sidebar['name'],
$sidebar['id']
),
'4.2.0'
);
}
$wp_registered_sidebars[ $sidebar['id'] ] = $sidebar;
add_theme_support( 'widgets' );
/**
* Fires once a sidebar has been registered.
*
* @since 3.0.0
*
* @param array $sidebar Parsed arguments for the registered sidebar.
*/
do_action( 'register_sidebar', $sidebar );
return $sidebar['id'];
}
/**
* Removes a sidebar from the list.
*
* @since 2.2.0
*
* @global array $wp_registered_sidebars The registered sidebars.
*
* @param string|int $sidebar_id The ID of the sidebar when it was registered.
*/
function unregister_sidebar( $sidebar_id ) {
global $wp_registered_sidebars;
unset( $wp_registered_sidebars[ $sidebar_id ] );
}
/**
* Checks if a sidebar is registered.
*
* @since 4.4.0
*
* @global array $wp_registered_sidebars The registered sidebars.
*
* @param string|int $sidebar_id The ID of the sidebar when it was registered.
* @return bool True if the sidebar is registered, false otherwise.
*/
function is_registered_sidebar( $sidebar_id ) {
global $wp_registered_sidebars;
return isset( $wp_registered_sidebars[ $sidebar_id ] );
}
/**
* Registers an instance of a widget.
*
* The default widget option is 'classname' that can be overridden.
*
* The function can also be used to un-register widgets when `$output_callback`
* parameter is an empty string.
*
* @since 2.2.0
* @since 5.3.0 Formalized the existing and already documented `...$params` parameter
* by adding it to the function signature.
* @since 5.8.0 Added show_instance_in_rest option.
*
* @global array $wp_registered_widgets Uses stored registered widgets.
* @global array $wp_registered_widget_controls Stores the registered widget controls (options).
* @global array $wp_registered_widget_updates The registered widget updates.
* @global array $_wp_deprecated_widgets_callbacks
*
* @param int|string $id Widget ID.
* @param string $name Widget display title.
* @param callable $output_callback Run when widget is called.
* @param array $options {
* Optional. An array of supplementary widget options for the instance.
*
* @type string $classname Class name for the widget's HTML container. Default is a shortened
* version of the output callback name.
* @type string $description Widget description for display in the widget administration
* panel and/or theme.
* @type bool $show_instance_in_rest Whether to show the widget's instance settings in the REST API.
* Only available for WP_Widget based widgets.
* }
* @param mixed ...$params Optional additional parameters to pass to the callback function when it's called.
*/
function wp_register_sidebar_widget( $id, $name, $output_callback, $options = array(), ...$params ) {
global $wp_registered_widgets, $wp_registered_widget_controls, $wp_registered_widget_updates, $_wp_deprecated_widgets_callbacks;
$id = strtolower( $id );
if ( empty( $output_callback ) ) {
unset( $wp_registered_widgets[ $id ] );
return;
}
$id_base = _get_widget_id_base( $id );
if ( in_array( $output_callback, $_wp_deprecated_widgets_callbacks, true ) && ! is_callable( $output_callback ) ) {
unset( $wp_registered_widget_controls[ $id ] );
unset( $wp_registered_widget_updates[ $id_base ] );
return;
}
$defaults = array( 'classname' => $output_callback );
$options = wp_parse_args( $options, $defaults );
$widget = array(
'name' => $name,
'id' => $id,
'callback' => $output_callback,
'params' => $params,
);
$widget = array_merge( $widget, $options );
if ( is_callable( $output_callback ) && ( ! isset( $wp_registered_widgets[ $id ] ) || did_action( 'widgets_init' ) ) ) {
/**
* Fires once for each registered widget.
*
* @since 3.0.0
*
* @param array $widget An array of default widget arguments.
*/
do_action( 'wp_register_sidebar_widget', $widget );
$wp_registered_widgets[ $id ] = $widget;
}
}
/**
* Retrieves description for widget.
*
* When registering widgets, the options can also include 'description' that
* describes the widget for display on the widget administration panel or
* in the theme.
*
* @since 2.5.0
*
* @global array $wp_registered_widgets The registered widgets.
*
* @param int|string $id Widget ID.
* @return string|void Widget description, if available.
*/
function wp_widget_description( $id ) {
if ( ! is_scalar( $id ) ) {
return;
}
global $wp_registered_widgets;
if ( isset( $wp_registered_widgets[ $id ]['description'] ) ) {
return esc_html( $wp_registered_widgets[ $id ]['description'] );
}
}
/**
* Retrieves description for a sidebar.
*
* When registering sidebars a 'description' parameter can be included that
* describes the sidebar for display on the widget administration panel.
*
* @since 2.9.0
*
* @global array $wp_registered_sidebars The registered sidebars.
*
* @param string $id sidebar ID.
* @return string|void Sidebar description, if available.
*/
function wp_sidebar_description( $id ) {
if ( ! is_scalar( $id ) ) {
return;
}
global $wp_registered_sidebars;
if ( isset( $wp_registered_sidebars[ $id ]['description'] ) ) {
return wp_kses( $wp_registered_sidebars[ $id ]['description'], 'sidebar_description' );
}
}
/**
* Remove widget from sidebar.
*
* @since 2.2.0
*
* @param int|string $id Widget ID.
*/
function wp_unregister_sidebar_widget( $id ) {
/**
* Fires just before a widget is removed from a sidebar.
*
* @since 3.0.0
*
* @param int|string $id The widget ID.
*/
do_action( 'wp_unregister_sidebar_widget', $id );
wp_register_sidebar_widget( $id, '', '' );
wp_unregister_widget_control( $id );
}
/**
* Registers widget control callback for customizing options.
*
* @since 2.2.0
* @since 5.3.0 Formalized the existing and already documented `...$params` parameter
* by adding it to the function signature.
*
* @global array $wp_registered_widget_controls The registered widget controls.
* @global array $wp_registered_widget_updates The registered widget updates.
* @global array $wp_registered_widgets The registered widgets.
* @global array $_wp_deprecated_widgets_callbacks
*
* @param int|string $id Sidebar ID.
* @param string $name Sidebar display name.
* @param callable $control_callback Run when sidebar is displayed.
* @param array $options {
* Optional. Array or string of control options. Default empty array.
*
* @type int $height Never used. Default 200.
* @type int $width Width of the fully expanded control form (but try hard to use the default width).
* Default 250.
* @type int|string $id_base Required for multi-widgets, i.e widgets that allow multiple instances such as the
* text widget. The widget ID will end up looking like `{$id_base}-{$unique_number}`.
* }
* @param mixed ...$params Optional additional parameters to pass to the callback function when it's called.
*/
function wp_register_widget_control( $id, $name, $control_callback, $options = array(), ...$params ) {
global $wp_registered_widget_controls, $wp_registered_widget_updates, $wp_registered_widgets, $_wp_deprecated_widgets_callbacks;
$id = strtolower( $id );
$id_base = _get_widget_id_base( $id );
if ( empty( $control_callback ) ) {
unset( $wp_registered_widget_controls[ $id ] );
unset( $wp_registered_widget_updates[ $id_base ] );
return;
}
if ( in_array( $control_callback, $_wp_deprecated_widgets_callbacks, true ) && ! is_callable( $control_callback ) ) {
unset( $wp_registered_widgets[ $id ] );
return;
}
if ( isset( $wp_registered_widget_controls[ $id ] ) && ! did_action( 'widgets_init' ) ) {
return;
}
$defaults = array(
'width' => 250,
'height' => 200,
); // Height is never used.
$options = wp_parse_args( $options, $defaults );
$options['width'] = (int) $options['width'];
$options['height'] = (int) $options['height'];
$widget = array(
'name' => $name,
'id' => $id,
'callback' => $control_callback,
'params' => $params,
);
$widget = array_merge( $widget, $options );
$wp_registered_widget_controls[ $id ] = $widget;
if ( isset( $wp_registered_widget_updates[ $id_base ] ) ) {
return;
}
if ( isset( $widget['params'][0]['number'] ) ) {
$widget['params'][0]['number'] = -1;
}
unset( $widget['width'], $widget['height'], $widget['name'], $widget['id'] );
$wp_registered_widget_updates[ $id_base ] = $widget;
}
/**
* Registers the update callback for a widget.
*
* @since 2.8.0
* @since 5.3.0 Formalized the existing and already documented `...$params` parameter
* by adding it to the function signature.
*
* @global array $wp_registered_widget_updates The registered widget updates.
*
* @param string $id_base The base ID of a widget created by extending WP_Widget.
* @param callable $update_callback Update callback method for the widget.
* @param array $options Optional. Widget control options. See wp_register_widget_control().
* Default empty array.
* @param mixed ...$params Optional additional parameters to pass to the callback function when it's called.
*/
function _register_widget_update_callback( $id_base, $update_callback, $options = array(), ...$params ) {
global $wp_registered_widget_updates;
if ( isset( $wp_registered_widget_updates[ $id_base ] ) ) {
if ( empty( $update_callback ) ) {
unset( $wp_registered_widget_updates[ $id_base ] );
}
return;
}
$widget = array(
'callback' => $update_callback,
'params' => $params,
);
$widget = array_merge( $widget, $options );
$wp_registered_widget_updates[ $id_base ] = $widget;
}
/**
* Registers the form callback for a widget.
*
* @since 2.8.0
* @since 5.3.0 Formalized the existing and already documented `...$params` parameter
* by adding it to the function signature.
*
* @global array $wp_registered_widget_controls The registered widget controls.
*
* @param int|string $id Widget ID.
* @param string $name Name attribute for the widget.
* @param callable $form_callback Form callback.
* @param array $options Optional. Widget control options. See wp_register_widget_control().
* Default empty array.
* @param mixed ...$params Optional additional parameters to pass to the callback function when it's called.
*/
function _register_widget_form_callback( $id, $name, $form_callback, $options = array(), ...$params ) {
global $wp_registered_widget_controls;
$id = strtolower( $id );
if ( empty( $form_callback ) ) {
unset( $wp_registered_widget_controls[ $id ] );
return;
}
if ( isset( $wp_registered_widget_controls[ $id ] ) && ! did_action( 'widgets_init' ) ) {
return;
}
$defaults = array(
'width' => 250,
'height' => 200,
);
$options = wp_parse_args( $options, $defaults );
$options['width'] = (int) $options['width'];
$options['height'] = (int) $options['height'];
$widget = array(
'name' => $name,
'id' => $id,
'callback' => $form_callback,
'params' => $params,
);
$widget = array_merge( $widget, $options );
$wp_registered_widget_controls[ $id ] = $widget;
}
/**
* Removes control callback for widget.
*
* @since 2.2.0
*
* @param int|string $id Widget ID.
*/
function wp_unregister_widget_control( $id ) {
wp_register_widget_control( $id, '', '' );
}
/**
* Displays dynamic sidebar.
*
* By default this displays the default sidebar or 'sidebar-1'. If your theme specifies the 'id' or
* 'name' parameter for its registered sidebars you can pass an ID or name as the $index parameter.
* Otherwise, you can pass in a numerical index to display the sidebar at that index.
*
* @since 2.2.0
*
* @global array $wp_registered_sidebars The registered sidebars.
* @global array $wp_registered_widgets The registered widgets.
*
* @param int|string $index Optional. Index, name or ID of dynamic sidebar. Default 1.
* @return bool True, if widget sidebar was found and called. False if not found or not called.
*/
function dynamic_sidebar( $index = 1 ) {
global $wp_registered_sidebars, $wp_registered_widgets;
if ( is_int( $index ) ) {
$index = "sidebar-$index";
} else {
$index = sanitize_title( $index );
foreach ( (array) $wp_registered_sidebars as $key => $value ) {
if ( sanitize_title( $value['name'] ) === $index ) {
$index = $key;
break;
}
}
}
$sidebars_widgets = wp_get_sidebars_widgets();
if ( empty( $wp_registered_sidebars[ $index ] ) || empty( $sidebars_widgets[ $index ] ) || ! is_array( $sidebars_widgets[ $index ] ) ) {
/** This action is documented in wp-includes/widget.php */
do_action( 'dynamic_sidebar_before', $index, false );
/** This action is documented in wp-includes/widget.php */
do_action( 'dynamic_sidebar_after', $index, false );
/** This filter is documented in wp-includes/widget.php */
return apply_filters( 'dynamic_sidebar_has_widgets', false, $index );
}
$sidebar = $wp_registered_sidebars[ $index ];
$sidebar['before_sidebar'] = sprintf( $sidebar['before_sidebar'], $sidebar['id'], $sidebar['class'] );
/**
* Fires before widgets are rendered in a dynamic sidebar.
*
* Note: The action also fires for empty sidebars, and on both the front end
* and back end, including the Inactive Widgets sidebar on the Widgets screen.
*
* @since 3.9.0
*
* @param int|string $index Index, name, or ID of the dynamic sidebar.
* @param bool $has_widgets Whether the sidebar is populated with widgets.
* Default true.
*/
do_action( 'dynamic_sidebar_before', $index, true );
if ( ! is_admin() && ! empty( $sidebar['before_sidebar'] ) ) {
echo $sidebar['before_sidebar'];
}
$did_one = false;
foreach ( (array) $sidebars_widgets[ $index ] as $id ) {
if ( ! isset( $wp_registered_widgets[ $id ] ) ) {
continue;
}
$params = array_merge(
array(
array_merge(
$sidebar,
array(
'widget_id' => $id,
'widget_name' => $wp_registered_widgets[ $id ]['name'],
)
),
),
(array) $wp_registered_widgets[ $id ]['params']
);
// Substitute HTML `id` and `class` attributes into `before_widget`.
$classname_ = '';
foreach ( (array) $wp_registered_widgets[ $id ]['classname'] as $cn ) {
if ( is_string( $cn ) ) {
$classname_ .= '_' . $cn;
} elseif ( is_object( $cn ) ) {
$classname_ .= '_' . get_class( $cn );
}
}
$classname_ = ltrim( $classname_, '_' );
$params[0]['before_widget'] = sprintf(
$params[0]['before_widget'],
str_replace( '\\', '_', $id ),
$classname_
);
/**
* Filters the parameters passed to a widget's display callback.
*
* Note: The filter is evaluated on both the front end and back end,
* including for the Inactive Widgets sidebar on the Widgets screen.
*
* @since 2.5.0
*
* @see register_sidebar()
*
* @param array $params {
* @type array $args {
* An array of widget display arguments.
*
* @type string $name Name of the sidebar the widget is assigned to.
* @type string $id ID of the sidebar the widget is assigned to.
* @type string $description The sidebar description.
* @type string $class CSS class applied to the sidebar container.
* @type string $before_widget HTML markup to prepend to each widget in the sidebar.
* @type string $after_widget HTML markup to append to each widget in the sidebar.
* @type string $before_title HTML markup to prepend to the widget title when displayed.
* @type string $after_title HTML markup to append to the widget title when displayed.
* @type string $widget_id ID of the widget.
* @type string $widget_name Name of the widget.
* }
* @type array $widget_args {
* An array of multi-widget arguments.
*
* @type int $number Number increment used for multiples of the same widget.
* }
* }
*/
$params = apply_filters( 'dynamic_sidebar_params', $params );
$callback = $wp_registered_widgets[ $id ]['callback'];
/**
* Fires before a widget's display callback is called.
*
* Note: The action fires on both the front end and back end, including
* for widgets in the Inactive Widgets sidebar on the Widgets screen.
*
* The action is not fired for empty sidebars.
*
* @since 3.0.0
*
* @param array $widget {
* An associative array of widget arguments.
*
* @type string $name Name of the widget.
* @type string $id Widget ID.
* @type callable $callback When the hook is fired on the front end, `$callback` is an array
* containing the widget object. Fired on the back end, `$callback`
* is 'wp_widget_control', see `$_callback`.
* @type array $params An associative array of multi-widget arguments.
* @type string $classname CSS class applied to the widget container.
* @type string $description The widget description.
* @type array $_callback When the hook is fired on the back end, `$_callback` is populated
* with an array containing the widget object, see `$callback`.
* }
*/
do_action( 'dynamic_sidebar', $wp_registered_widgets[ $id ] );
if ( is_callable( $callback ) ) {
call_user_func_array( $callback, $params );
$did_one = true;
}
}
if ( ! is_admin() && ! empty( $sidebar['after_sidebar'] ) ) {
echo $sidebar['after_sidebar'];
}
/**
* Fires after widgets are rendered in a dynamic sidebar.
*
* Note: The action also fires for empty sidebars, and on both the front end
* and back end, including the Inactive Widgets sidebar on the Widgets screen.
*
* @since 3.9.0
*
* @param int|string $index Index, name, or ID of the dynamic sidebar.
* @param bool $has_widgets Whether the sidebar is populated with widgets.
* Default true.
*/
do_action( 'dynamic_sidebar_after', $index, true );
/**
* Filters whether a sidebar has widgets.
*
* Note: The filter is also evaluated for empty sidebars, and on both the front end
* and back end, including the Inactive Widgets sidebar on the Widgets screen.
*
* @since 3.9.0
*
* @param bool $did_one Whether at least one widget was rendered in the sidebar.
* Default false.
* @param int|string $index Index, name, or ID of the dynamic sidebar.
*/
return apply_filters( 'dynamic_sidebar_has_widgets', $did_one, $index );
}
/**
* Determines whether a given widget is displayed on the front end.
*
* Either $callback or $id_base can be used.
* $id_base is the first argument when extending WP_Widget class.
* Without the optional $widget_id parameter, returns the ID of the first sidebar
* in which the first instance of the widget with the given callback or $id_base is found.
* With the $widget_id parameter, returns the ID of the sidebar where
* the widget with that callback/$id_base AND that ID is found.
*
* NOTE: $widget_id and $id_base are the same for single widgets. To be effective
* this function has to run after widgets have initialized, at action {@see 'init'} or later.
*
* 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.2.0
*
* @global array $wp_registered_widgets The registered widgets.
*
* @param callable|false $callback Optional. Widget callback to check. Default false.
* @param string|false $widget_id Optional. Widget ID. Optional, but needed for checking.
* Default false.
* @param string|false $id_base Optional. The base ID of a widget created by extending WP_Widget.
* Default false.
* @param bool $skip_inactive Optional. Whether to check in 'wp_inactive_widgets'.
* Default true.
* @return string|false ID of the sidebar in which the widget is active,
* false if the widget is not active.
*/
function is_active_widget( $callback = false, $widget_id = false, $id_base = false, $skip_inactive = true ) {
global $wp_registered_widgets;
$sidebars_widgets = wp_get_sidebars_widgets();
if ( is_array( $sidebars_widgets ) ) {
foreach ( $sidebars_widgets as $sidebar => $widgets ) {
if ( $skip_inactive && ( 'wp_inactive_widgets' === $sidebar || str_starts_with( $sidebar, 'orphaned_widgets' ) ) ) {
continue;
}
if ( is_array( $widgets ) ) {
foreach ( $widgets as $widget ) {
if ( ( $callback && isset( $wp_registered_widgets[ $widget ]['callback'] ) && $wp_registered_widgets[ $widget ]['callback'] === $callback ) || ( $id_base && _get_widget_id_base( $widget ) === $id_base ) ) {
if ( ! $widget_id || $widget_id === $wp_registered_widgets[ $widget ]['id'] ) {
return $sidebar;
}
}
}
}
}
}
return false;
}
/**
* Determines whether the dynamic sidebar is enabled and used by the theme.
*
* 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.2.0
*
* @global array $wp_registered_widgets The registered widgets.
* @global array $wp_registered_sidebars The registered sidebars.
*
* @return bool True if using widgets, false otherwise.
*/
function is_dynamic_sidebar() {
global $wp_registered_widgets, $wp_registered_sidebars;
$sidebars_widgets = get_option( 'sidebars_widgets' );
foreach ( (array) $wp_registered_sidebars as $index => $sidebar ) {
if ( ! empty( $sidebars_widgets[ $index ] ) ) {
foreach ( (array) $sidebars_widgets[ $index ] as $widget ) {
if ( array_key_exists( $widget, $wp_registered_widgets ) ) {
return true;
}
}
}
}
return false;
}
/**
* Determines whether a sidebar contains widgets.
*
* 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
*
* @param string|int $index Sidebar name, id or number to check.
* @return bool True if the sidebar has widgets, false otherwise.
*/
function is_active_sidebar( $index ) {
$index = ( is_int( $index ) ) ? "sidebar-$index" : sanitize_title( $index );
$sidebars_widgets = wp_get_sidebars_widgets();
$is_active_sidebar = ! empty( $sidebars_widgets[ $index ] );
/**
* Filters whether a dynamic sidebar is considered "active".
*
* @since 3.9.0
*
* @param bool $is_active_sidebar Whether or not the sidebar should be considered "active".
* In other words, whether the sidebar contains any widgets.
* @param int|string $index Index, name, or ID of the dynamic sidebar.
*/
return apply_filters( 'is_active_sidebar', $is_active_sidebar, $index );
}
//
// Internal Functions.
//
/**
* Retrieves the full list of sidebars and their widget instance IDs.
*
* Will upgrade sidebar widget list, if needed. Will also save updated list, if
* needed.
*
* @since 2.2.0
* @access private
*
* @global array $_wp_sidebars_widgets
* @global array $sidebars_widgets
*
* @param bool $deprecated Not used (argument deprecated).
* @return array Upgraded list of widgets to version 3 array format when called from the admin.
*/
function wp_get_sidebars_widgets( $deprecated = true ) {
if ( true !== $deprecated ) {
_deprecated_argument( __FUNCTION__, '2.8.1' );
}
global $_wp_sidebars_widgets, $sidebars_widgets;
/*
* If loading from front page, consult $_wp_sidebars_widgets rather than options
* to see if wp_convert_widget_settings() has made manipulations in memory.
*/
if ( ! is_admin() ) {
if ( empty( $_wp_sidebars_widgets ) ) {
$_wp_sidebars_widgets = get_option( 'sidebars_widgets', array() );
}
$sidebars_widgets = $_wp_sidebars_widgets;
} else {
$sidebars_widgets = get_option( 'sidebars_widgets', array() );
}
if ( is_array( $sidebars_widgets ) && isset( $sidebars_widgets['array_version'] ) ) {
unset( $sidebars_widgets['array_version'] );
}
/**
* Filters the list of sidebars and their widgets.
*
* @since 2.7.0
*
* @param array $sidebars_widgets An associative array of sidebars and their widgets.
*/
return apply_filters( 'sidebars_widgets', $sidebars_widgets );
}
/**
* Retrieves the registered sidebar with the given ID.
*
* @since 5.9.0
*
* @global array $wp_registered_sidebars The registered sidebars.
*
* @param string $id The sidebar ID.
* @return array|null The discovered sidebar, or null if it is not registered.
*/
function wp_get_sidebar( $id ) {
global $wp_registered_sidebars;
foreach ( (array) $wp_registered_sidebars as $sidebar ) {
if ( $sidebar['id'] === $id ) {
return $sidebar;
}
}
if ( 'wp_inactive_widgets' === $id ) {
return array(
'id' => 'wp_inactive_widgets',
'name' => __( 'Inactive widgets' ),
);
}
return null;
}
/**
* Sets the sidebar widget option to update sidebars.
*
* @since 2.2.0
* @access private
*
* @global array $_wp_sidebars_widgets
* @param array $sidebars_widgets Sidebar widgets and their settings.
*/
function wp_set_sidebars_widgets( $sidebars_widgets ) {
global $_wp_sidebars_widgets;
// Clear cached value used in wp_get_sidebars_widgets().
$_wp_sidebars_widgets = null;
if ( ! isset( $sidebars_widgets['array_version'] ) ) {
$sidebars_widgets['array_version'] = 3;
}
update_option( 'sidebars_widgets', $sidebars_widgets );
}
/**
* Retrieves default registered sidebars list.
*
* @since 2.2.0
* @access private
*
* @global array $wp_registered_sidebars The registered sidebars.
*
* @return array
*/
function wp_get_widget_defaults() {
global $wp_registered_sidebars;
$defaults = array();
foreach ( (array) $wp_registered_sidebars as $index => $sidebar ) {
$defaults[ $index ] = array();
}
return $defaults;
}
/**
* Converts the widget settings from single to multi-widget format.
*
* @since 2.8.0
*
* @global array $_wp_sidebars_widgets
*
* @param string $base_name Root ID for all widgets of this type.
* @param string $option_name Option name for this widget type.
* @param array $settings The array of widget instance settings.
* @return array The array of widget settings converted to multi-widget format.
*/
function wp_convert_widget_settings( $base_name, $option_name, $settings ) {
// This test may need expanding.
$single = false;
$changed = false;
if ( empty( $settings ) ) {
$single = true;
} else {
foreach ( array_keys( $settings ) as $number ) {
if ( 'number' === $number ) {
continue;
}
if ( ! is_numeric( $number ) ) {
$single = true;
break;
}
}
}
if ( $single ) {
$settings = array( 2 => $settings );
// If loading from the front page, update sidebar in memory but don't save to options.
if ( is_admin() ) {
$sidebars_widgets = get_option( 'sidebars_widgets' );
} else {
if ( empty( $GLOBALS['_wp_sidebars_widgets'] ) ) {
$GLOBALS['_wp_sidebars_widgets'] = get_option( 'sidebars_widgets', array() );
}
$sidebars_widgets = &$GLOBALS['_wp_sidebars_widgets'];
}
foreach ( (array) $sidebars_widgets as $index => $sidebar ) {
if ( is_array( $sidebar ) ) {
foreach ( $sidebar as $i => $name ) {
if ( $base_name === $name ) {
$sidebars_widgets[ $index ][ $i ] = "$name-2";
$changed = true;
break 2;
}
}
}
}
if ( is_admin() && $changed ) {
update_option( 'sidebars_widgets', $sidebars_widgets );
}
}
$settings['_multiwidget'] = 1;
if ( is_admin() ) {
update_option( $option_name, $settings );
}
return $settings;
}
/**
* Outputs an arbitrary widget as a template tag.
*
* @since 2.8.0
*
* @global WP_Widget_Factory $wp_widget_factory
*
* @param string $widget The widget's PHP class name (see class-wp-widget.php).
* @param array $instance Optional. The widget's instance settings. Default empty array.
* @param array $args {
* Optional. Array of arguments to configure the display of the widget.
*
* @type string $before_widget HTML content that will be prepended to the widget's HTML output.
* Default ``.
* @type string $before_title HTML content that will be prepended to the widget's title when displayed.
* Default `register_widget()'
),
'4.9.0'
);
return;
}
$widget_obj = $wp_widget_factory->widgets[ $widget ];
if ( ! ( $widget_obj instanceof WP_Widget ) ) {
return;
}
$default_args = array(
'before_widget' => '',
'before_title' => '' . __( 'RSS Error:' ) . ' ' . esc_html( $rss->get_error_message() ) . '
'; } return; } $default_args = array( 'show_author' => 0, 'show_date' => 0, 'show_summary' => 0, 'items' => 0, ); $args = wp_parse_args( $args, $default_args ); $items = (int) $args['items']; if ( $items < 1 || 20 < $items ) { $items = 10; } $show_summary = (int) $args['show_summary']; $show_author = (int) $args['show_author']; $show_date = (int) $args['show_date']; if ( ! $rss->get_item_quantity() ) { echo '
/>
/>
/>
]*>.+?)#i', $tokenized, $matches, PREG_SET_ORDER ); foreach ( $matches as $i => $match ) { $tag_html = str_replace( $replace, $search, $match[0] ); $tag_token = $token . $i; $found[ $tag_token ] = $tag_html; $html = str_replace( $tag_html, $tag_token, $html, $count ); } $replaced = str_replace( $replace, $search, $html ); $stripped = str_replace( array( "\r\n", "\n" ), '', $replaced ); $pre = array_values( $found ); $tokens = array_keys( $found ); return str_replace( $tokens, $pre, $stripped ); } } data = $data; } /** * PHP4 constructor. */ public function IXR_Base64( $data ) { self::__construct( $data ); } function getXml() { return '
'.htmlspecialchars($request)."\n\n\n"; } if ($this->timeout) { $fp = @fsockopen($this->server, $this->port, $errno, $errstr, $this->timeout); } else { $fp = @fsockopen($this->server, $this->port, $errno, $errstr); } if (!$fp) { $this->error = new IXR_Error(-32300, 'transport error - could not open socket'); return false; } fputs($fp, $request); $contents = ''; $debugContents = ''; $gotFirstLine = false; $gettingHeaders = true; while (!feof($fp)) { $line = fgets($fp, 4096); if (!$gotFirstLine) { // Check line for '200' if (strstr($line, '200') === false) { $this->error = new IXR_Error(-32300, 'transport error - HTTP status code was not 200'); return false; } $gotFirstLine = true; } if (trim($line) == '') { $gettingHeaders = false; } if (!$gettingHeaders) { // merged from WP #12559 - remove trim $contents .= $line; } if ($this->debug) { $debugContents .= $line; } } if ($this->debug) { echo '
'.htmlspecialchars($debugContents)."\n\n\n"; } // Now parse what we've got back $this->message = new IXR_Message($contents); if (!$this->message->parse()) { // XML error $this->error = new IXR_Error(-32700, 'parse error. not well formed'); return false; } // Is the message a fault? if ($this->message->messageType == 'fault') { $this->error = new IXR_Error($this->message->faultCode, $this->message->faultString); return false; } // Message must be OK return true; } function getResponse() { // methodResponses can only have one param - return that return $this->message->params[0]; } function isError() { return (is_object($this->error)); } function getErrorCode() { return $this->error->code; } function getErrorMessage() { return $this->error->message; } } parseTimestamp($time); } else { $this->parseIso($time); } } /** * PHP4 constructor. */ public function IXR_Date( $time ) { self::__construct( $time ); } function parseTimestamp($timestamp) { $this->year = gmdate('Y', $timestamp); $this->month = gmdate('m', $timestamp); $this->day = gmdate('d', $timestamp); $this->hour = gmdate('H', $timestamp); $this->minute = gmdate('i', $timestamp); $this->second = gmdate('s', $timestamp); $this->timezone = ''; } function parseIso($iso) { $this->year = substr($iso, 0, 4); $this->month = substr($iso, 4, 2); $this->day = substr($iso, 6, 2); $this->hour = substr($iso, 9, 2); $this->minute = substr($iso, 12, 2); $this->second = substr($iso, 15, 2); $this->timezone = substr($iso, 17); } function getIso() { return $this->year.$this->month.$this->day.'T'.$this->hour.':'.$this->minute.':'.$this->second.$this->timezone; } function getXml() { return '
' . htmlspecialchars( $xml ) . "\n\n\n"; } $response = wp_safe_remote_post( $url, $args ); if ( is_wp_error( $response ) ) { $errno = $response->get_error_code(); $errorstr = $response->get_error_message(); $this->error = new IXR_Error( -32300, "transport error: $errno $errorstr" ); return false; } if ( 200 !== wp_remote_retrieve_response_code( $response ) ) { $this->error = new IXR_Error( -32301, 'transport error - HTTP status code was not 200 (' . wp_remote_retrieve_response_code( $response ) . ')' ); return false; } if ( $this->debug ) { echo '
' . htmlspecialchars( wp_remote_retrieve_body( $response ) ) . "\n\n\n"; } // Now parse what we've got back. $this->message = new IXR_Message( wp_remote_retrieve_body( $response ) ); if ( ! $this->message->parse() ) { // XML error. $this->error = new IXR_Error( -32700, 'parse error. not well formed' ); return false; } // Is the message a fault? if ( 'fault' === $this->message->messageType ) { $this->error = new IXR_Error( $this->message->faultCode, $this->message->faultString ); return false; } // Message must be OK. return true; } } cookie_service = $cookie_service; $this->key_service = $key_service; } /** * Generates a URL to begin recovery mode. * * Only one recovery mode URL can may be valid at the same time. * * @since 5.2.0 * * @return string Generated URL. */ public function generate_url() { $token = $this->key_service->generate_recovery_mode_token(); $key = $this->key_service->generate_and_store_recovery_mode_key( $token ); return $this->get_recovery_mode_begin_url( $token, $key ); } /** * Enters recovery mode when the user hits wp-login.php with a valid recovery mode link. * * @since 5.2.0 * * @global string $pagenow The filename of the current screen. * * @param int $ttl Number of seconds the link should be valid for. */ public function handle_begin_link( $ttl ) { if ( ! isset( $GLOBALS['pagenow'] ) || 'wp-login.php' !== $GLOBALS['pagenow'] ) { return; } if ( ! isset( $_GET['action'], $_GET['rm_token'], $_GET['rm_key'] ) || self::LOGIN_ACTION_ENTER !== $_GET['action'] ) { return; } if ( ! function_exists( 'wp_generate_password' ) ) { require_once ABSPATH . WPINC . '/pluggable.php'; } $validated = $this->key_service->validate_recovery_mode_key( $_GET['rm_token'], $_GET['rm_key'], $ttl ); if ( is_wp_error( $validated ) ) { wp_die( $validated, '' ); } $this->cookie_service->set_cookie(); $url = add_query_arg( 'action', self::LOGIN_ACTION_ENTERED, wp_login_url() ); wp_redirect( $url ); die; } /** * Gets a URL to begin recovery mode. * * @since 5.2.0 * * @param string $token Recovery Mode token created by {@see generate_recovery_mode_token()}. * @param string $key Recovery Mode key created by {@see generate_and_store_recovery_mode_key()}. * @return string Recovery mode begin URL. */ private function get_recovery_mode_begin_url( $token, $key ) { $url = add_query_arg( array( 'action' => self::LOGIN_ACTION_ENTER, 'rm_token' => $token, 'rm_key' => $key, ), wp_login_url() ); /** * Filters the URL to begin recovery mode. * * @since 5.2.0 * * @param string $url The generated recovery mode begin URL. * @param string $token The token used to identify the key. * @param string $key The recovery mode key. */ return apply_filters( 'recovery_mode_begin_url', $url, $token, $key ); } } url = $url; $this->request = $request; } /** * Dispatch a Requests hook to a native WordPress action. * * @param string $hook Hook name. * @param array $parameters Parameters to pass to callbacks. * @return bool True if hooks were run, false if nothing was hooked. */ public function dispatch( $hook, $parameters = array() ) { $result = parent::dispatch( $hook, $parameters ); // Handle back-compat actions. switch ( $hook ) { case 'curl.before_send': /** This action is documented in wp-includes/class-wp-http-curl.php */ do_action_ref_array( 'http_api_curl', array( &$parameters[0], $this->request, $this->url ) ); break; } /** * Transforms a native Request hook to a WordPress action. * * This action maps Requests internal hook to a native WordPress action. * * @see https://github.com/WordPress/Requests/blob/master/docs/hooks.md * * @since 4.7.0 * * @param array $parameters Parameters from Requests internal hook. * @param array $request Request data in WP_Http format. * @param string $url URL to request. */ do_action_ref_array( "requests-{$hook}", $parameters, $this->request, $this->url ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores return $result; } } ID : false; } /** * Displays or retrieves the current post title with optional markup. * * @since 0.71 * * @param string $before Optional. Markup to prepend to the title. Default empty. * @param string $after Optional. Markup to append to the title. Default empty. * @param bool $display Optional. Whether to echo or return the title. Default true for echo. * @return void|string Void if `$display` argument is true or the title is empty, * current post title if `$display` is false. */ function the_title( $before = '', $after = '', $display = true ) { $title = get_the_title(); if ( strlen( $title ) === 0 ) { return; } $title = $before . $title . $after; if ( $display ) { echo $title; } else { return $title; } } /** * Sanitizes the current title when retrieving or displaying. * * Works like the_title(), except the parameters can be in a string or * an array. See the function for what can be override in the $args parameter. * * The title before it is displayed will have the tags stripped and esc_attr() * before it is passed to the user or displayed. The default as with the_title(), * is to display the title. * * @since 2.3.0 * * @param string|array $args { * Title attribute arguments. Optional. * * @type string $before Markup to prepend to the title. Default empty. * @type string $after Markup to append to the title. Default empty. * @type bool $echo Whether to echo or return the title. Default true for echo. * @type WP_Post $post Current post object to retrieve the title for. * } * @return void|string Void if 'echo' argument is true, the title attribute if 'echo' is false. */ function the_title_attribute( $args = '' ) { $defaults = array( 'before' => '', 'after' => '', 'echo' => true, 'post' => get_post(), ); $parsed_args = wp_parse_args( $args, $defaults ); $title = get_the_title( $parsed_args['post'] ); if ( strlen( $title ) === 0 ) { return; } $title = $parsed_args['before'] . $title . $parsed_args['after']; $title = esc_attr( strip_tags( $title ) ); if ( $parsed_args['echo'] ) { echo $title; } else { return $title; } } /** * Retrieves the post title. * * If the post is protected and the visitor is not an admin, then "Protected" * will be inserted before the post title. If the post is private, then * "Private" will be inserted before the post title. * * @since 0.71 * * @param int|WP_Post $post Optional. Post ID or WP_Post object. Default is global $post. * @return string */ function get_the_title( $post = 0 ) { $post = get_post( $post ); $post_title = isset( $post->post_title ) ? $post->post_title : ''; $post_id = isset( $post->ID ) ? $post->ID : 0; if ( ! is_admin() ) { if ( ! empty( $post->post_password ) ) { /* translators: %s: Protected post title. */ $prepend = __( 'Protected: %s' ); /** * Filters the text prepended to the post title for protected posts. * * The filter is only applied on the front end. * * @since 2.8.0 * * @param string $prepend Text displayed before the post title. * Default 'Protected: %s'. * @param WP_Post $post Current post object. */ $protected_title_format = apply_filters( 'protected_title_format', $prepend, $post ); $post_title = sprintf( $protected_title_format, $post_title ); } elseif ( isset( $post->post_status ) && 'private' === $post->post_status ) { /* translators: %s: Private post title. */ $prepend = __( 'Private: %s' ); /** * Filters the text prepended to the post title of private posts. * * The filter is only applied on the front end. * * @since 2.8.0 * * @param string $prepend Text displayed before the post title. * Default 'Private: %s'. * @param WP_Post $post Current post object. */ $private_title_format = apply_filters( 'private_title_format', $prepend, $post ); $post_title = sprintf( $private_title_format, $post_title ); } } /** * Filters the post title. * * @since 0.71 * * @param string $post_title The post title. * @param int $post_id The post ID. */ return apply_filters( 'the_title', $post_title, $post_id ); } /** * Displays the Post Global Unique Identifier (guid). * * The guid will appear to be a link, but should not be used as a link to the * post. The reason you should not use it as a link, is because of moving the * blog across domains. * * URL is escaped to make it XML-safe. * * @since 1.5.0 * * @param int|WP_Post $post Optional. Post ID or post object. Default is global $post. */ function the_guid( $post = 0 ) { $post = get_post( $post ); $post_guid = isset( $post->guid ) ? get_the_guid( $post ) : ''; $post_id = isset( $post->ID ) ? $post->ID : 0; /** * Filters the escaped Global Unique Identifier (guid) of the post. * * @since 4.2.0 * * @see get_the_guid() * * @param string $post_guid Escaped Global Unique Identifier (guid) of the post. * @param int $post_id The post ID. */ echo apply_filters( 'the_guid', $post_guid, $post_id ); } /** * Retrieves the Post Global Unique Identifier (guid). * * The guid will appear to be a link, but should not be used as an link to the * post. The reason you should not use it as a link, is because of moving the * blog across domains. * * @since 1.5.0 * * @param int|WP_Post $post Optional. Post ID or post object. Default is global $post. * @return string */ function get_the_guid( $post = 0 ) { $post = get_post( $post ); $post_guid = isset( $post->guid ) ? $post->guid : ''; $post_id = isset( $post->ID ) ? $post->ID : 0; /** * Filters the Global Unique Identifier (guid) of the post. * * @since 1.5.0 * * @param string $post_guid Global Unique Identifier (guid) of the post. * @param int $post_id The post ID. */ return apply_filters( 'get_the_guid', $post_guid, $post_id ); } /** * Displays the post content. * * @since 0.71 * * @param string $more_link_text Optional. Content for when there is more text. * @param bool $strip_teaser Optional. Strip teaser content before the more text. Default false. */ function the_content( $more_link_text = null, $strip_teaser = false ) { $content = get_the_content( $more_link_text, $strip_teaser ); /** * Filters the post content. * * @since 0.71 * * @param string $content Content of the current post. */ $content = apply_filters( 'the_content', $content ); $content = str_replace( ']]>', ']]>', $content ); echo $content; } /** * Retrieves the post content. * * @since 0.71 * @since 5.2.0 Added the `$post` parameter. * * @global int $page Page number of a single post/page. * @global int $more Boolean indicator for whether single post/page is being viewed. * @global bool $preview Whether post/page is in preview mode. * @global array $pages Array of all pages in post/page. Each array element contains * part of the content separated by the `` tag. * @global int $multipage Boolean indicator for whether multiple pages are in play. * * @param string $more_link_text Optional. Content for when there is more text. * @param bool $strip_teaser Optional. Strip teaser content before the more text. Default false. * @param WP_Post|object|int $post Optional. WP_Post instance or Post ID/object. Default null. * @return string */ function get_the_content( $more_link_text = null, $strip_teaser = false, $post = null ) { global $page, $more, $preview, $pages, $multipage; $_post = get_post( $post ); if ( ! ( $_post instanceof WP_Post ) ) { return ''; } /* * Use the globals if the $post parameter was not specified, * but only after they have been set up in setup_postdata(). */ if ( null === $post && did_action( 'the_post' ) ) { $elements = compact( 'page', 'more', 'preview', 'pages', 'multipage' ); } else { $elements = generate_postdata( $_post ); } if ( null === $more_link_text ) { $more_link_text = sprintf( '%2$s', sprintf( /* translators: %s: Post title. */ __( 'Continue reading %s' ), the_title_attribute( array( 'echo' => false, 'post' => $_post, ) ) ), __( '(more…)' ) ); } $output = ''; $has_teaser = false; // If post password required and it doesn't match the cookie. if ( post_password_required( $_post ) ) { return get_the_password_form( $_post ); } // If the requested page doesn't exist. if ( $elements['page'] > count( $elements['pages'] ) ) { // Give them the highest numbered page that DOES exist. $elements['page'] = count( $elements['pages'] ); } $page_no = $elements['page']; $content = $elements['pages'][ $page_no - 1 ]; if ( preg_match( '//', $content, $matches ) ) { if ( has_block( 'more', $content ) ) { // Remove the core/more block delimiters. They will be left over after $content is split up. $content = preg_replace( '//', '', $content ); } $content = explode( $matches[0], $content, 2 ); if ( ! empty( $matches[1] ) && ! empty( $more_link_text ) ) { $more_link_text = strip_tags( wp_kses_no_null( trim( $matches[1] ) ) ); } $has_teaser = true; } else { $content = array( $content ); } if ( str_contains( $_post->post_content, '' ) && ( ! $elements['multipage'] || 1 === $elements['page'] ) ) { $strip_teaser = true; } $teaser = $content[0]; if ( $elements['more'] && $strip_teaser && $has_teaser ) { $teaser = ''; } $output .= $teaser; if ( count( $content ) > 1 ) { if ( $elements['more'] ) { $output .= '' . $content[1]; } else { if ( ! empty( $more_link_text ) ) { /** * Filters the Read More link text. * * @since 2.8.0 * * @param string $more_link_element Read More link element. * @param string $more_link_text Read More text. */ $output .= apply_filters( 'the_content_more_link', ' ID}\" class=\"more-link\">$more_link_text", $more_link_text ); } $output = force_balance_tags( $output ); } } return $output; } /** * Displays the post excerpt. * * @since 0.71 */ function the_excerpt() { /** * Filters the displayed post excerpt. * * @since 0.71 * * @see get_the_excerpt() * * @param string $post_excerpt The post excerpt. */ echo apply_filters( 'the_excerpt', get_the_excerpt() ); } /** * Retrieves the post excerpt. * * @since 0.71 * @since 4.5.0 Introduced the `$post` parameter. * * @param int|WP_Post $post Optional. Post ID or WP_Post object. Default is global $post. * @return string Post excerpt. */ function get_the_excerpt( $post = null ) { if ( is_bool( $post ) ) { _deprecated_argument( __FUNCTION__, '2.3.0' ); } $post = get_post( $post ); if ( empty( $post ) ) { return ''; } if ( post_password_required( $post ) ) { return __( 'There is no excerpt because this is a protected post.' ); } /** * Filters the retrieved post excerpt. * * @since 1.2.0 * @since 4.5.0 Introduced the `$post` parameter. * * @param string $post_excerpt The post excerpt. * @param WP_Post $post Post object. */ return apply_filters( 'get_the_excerpt', $post->post_excerpt, $post ); } /** * Determines whether the post has a custom excerpt. * * 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.3.0 * * @param int|WP_Post $post Optional. Post ID or WP_Post object. Default is global $post. * @return bool True if the post has a custom excerpt, false otherwise. */ function has_excerpt( $post = 0 ) { $post = get_post( $post ); return ( ! empty( $post->post_excerpt ) ); } /** * Displays the classes for the post container element. * * @since 2.7.0 * * @param string|string[] $css_class Optional. One or more classes to add to the class list. * Default empty. * @param int|WP_Post $post Optional. Post ID or post object. Defaults to the global `$post`. */ function post_class( $css_class = '', $post = null ) { // Separates classes with a single space, collates classes for post DIV. echo 'class="' . esc_attr( implode( ' ', get_post_class( $css_class, $post ) ) ) . '"'; } /** * Retrieves an array of the class names for the post container element. * * The class names are many: * * - If the post has a post thumbnail, `has-post-thumbnail` is added as a class. * - If the post is sticky, then the `sticky` class name is added. * - The class `hentry` is always added to each post. * - For each taxonomy that the post belongs to, a class will be added of the format * `{$taxonomy}-{$slug}`, e.g. `category-foo` or `my_custom_taxonomy-bar`. * The `post_tag` taxonomy is a special case; the class has the `tag-` prefix * instead of `post_tag-`. * * All class names are passed through the filter, {@see 'post_class'}, followed by * `$css_class` parameter value, with the post ID as the last parameter. * * @since 2.7.0 * @since 4.2.0 Custom taxonomy class names were added. * * @param string|string[] $css_class Optional. Space-separated string or array of class names * to add to the class list. Default empty. * @param int|WP_Post $post Optional. Post ID or post object. * @return string[] Array of class names. */ function get_post_class( $css_class = '', $post = null ) { $post = get_post( $post ); $classes = array(); if ( $css_class ) { if ( ! is_array( $css_class ) ) { $css_class = preg_split( '#\s+#', $css_class ); } $classes = array_map( 'esc_attr', $css_class ); } else { // Ensure that we always coerce class to being an array. $css_class = array(); } if ( ! $post ) { return $classes; } $classes[] = 'post-' . $post->ID; if ( ! is_admin() ) { $classes[] = $post->post_type; } $classes[] = 'type-' . $post->post_type; $classes[] = 'status-' . $post->post_status; // Post Format. if ( post_type_supports( $post->post_type, 'post-formats' ) ) { $post_format = get_post_format( $post->ID ); if ( $post_format && ! is_wp_error( $post_format ) ) { $classes[] = 'format-' . sanitize_html_class( $post_format ); } else { $classes[] = 'format-standard'; } } $post_password_required = post_password_required( $post->ID ); // Post requires password. if ( $post_password_required ) { $classes[] = 'post-password-required'; } elseif ( ! empty( $post->post_password ) ) { $classes[] = 'post-password-protected'; } // Post thumbnails. if ( current_theme_supports( 'post-thumbnails' ) && has_post_thumbnail( $post->ID ) && ! is_attachment( $post ) && ! $post_password_required ) { $classes[] = 'has-post-thumbnail'; } // Sticky for Sticky Posts. if ( is_sticky( $post->ID ) ) { if ( is_home() && ! is_paged() ) { $classes[] = 'sticky'; } elseif ( is_admin() ) { $classes[] = 'status-sticky'; } } // hentry for hAtom compliance. $classes[] = 'hentry'; // All public taxonomies. $taxonomies = get_taxonomies( array( 'public' => true ) ); /** * Filters the taxonomies to generate classes for each individual term. * * Default is all public taxonomies registered to the post type. * * @since 6.1.0 * * @param string[] $taxonomies List of all taxonomy names to generate classes for. * @param int $post_id The post ID. * @param string[] $classes An array of post class names. * @param string[] $css_class An array of additional class names added to the post. */ $taxonomies = apply_filters( 'post_class_taxonomies', $taxonomies, $post->ID, $classes, $css_class ); foreach ( (array) $taxonomies as $taxonomy ) { if ( is_object_in_taxonomy( $post->post_type, $taxonomy ) ) { foreach ( (array) get_the_terms( $post->ID, $taxonomy ) as $term ) { if ( empty( $term->slug ) ) { continue; } $term_class = sanitize_html_class( $term->slug, $term->term_id ); if ( is_numeric( $term_class ) || ! trim( $term_class, '-' ) ) { $term_class = $term->term_id; } // 'post_tag' uses the 'tag' prefix for backward compatibility. if ( 'post_tag' === $taxonomy ) { $classes[] = 'tag-' . $term_class; } else { $classes[] = sanitize_html_class( $taxonomy . '-' . $term_class, $taxonomy . '-' . $term->term_id ); } } } } $classes = array_map( 'esc_attr', $classes ); /** * Filters the list of CSS class names for the current post. * * @since 2.7.0 * * @param string[] $classes An array of post class names. * @param string[] $css_class An array of additional class names added to the post. * @param int $post_id The post ID. */ $classes = apply_filters( 'post_class', $classes, $css_class, $post->ID ); return array_unique( $classes ); } /** * Displays the class names for the body element. * * @since 2.8.0 * * @param string|string[] $css_class Optional. Space-separated string or array of class names * to add to the class list. Default empty. */ function body_class( $css_class = '' ) { // Separates class names with a single space, collates class names for body element. echo 'class="' . esc_attr( implode( ' ', get_body_class( $css_class ) ) ) . '"'; } /** * Retrieves an array of the class names for the body element. * * @since 2.8.0 * * @global WP_Query $wp_query WordPress Query object. * * @param string|string[] $css_class Optional. Space-separated string or array of class names * to add to the class list. Default empty. * @return string[] Array of class names. */ function get_body_class( $css_class = '' ) { global $wp_query; $classes = array(); if ( is_rtl() ) { $classes[] = 'rtl'; } if ( is_front_page() ) { $classes[] = 'home'; } if ( is_home() ) { $classes[] = 'blog'; } if ( is_privacy_policy() ) { $classes[] = 'privacy-policy'; } if ( is_archive() ) { $classes[] = 'archive'; } if ( is_date() ) { $classes[] = 'date'; } if ( is_search() ) { $classes[] = 'search'; $classes[] = $wp_query->posts ? 'search-results' : 'search-no-results'; } if ( is_paged() ) { $classes[] = 'paged'; } if ( is_attachment() ) { $classes[] = 'attachment'; } if ( is_404() ) { $classes[] = 'error404'; } if ( is_singular() ) { $post = $wp_query->get_queried_object(); $post_id = $post->ID; $post_type = $post->post_type; $classes[] = 'wp-singular'; if ( is_page_template() ) { $classes[] = "{$post_type}-template"; $template_slug = get_page_template_slug( $post_id ); $template_parts = explode( '/', $template_slug ); foreach ( $template_parts as $part ) { $classes[] = "{$post_type}-template-" . sanitize_html_class( str_replace( array( '.', '/' ), '-', basename( $part, '.php' ) ) ); } $classes[] = "{$post_type}-template-" . sanitize_html_class( str_replace( '.', '-', $template_slug ) ); } else { $classes[] = "{$post_type}-template-default"; } if ( is_single() ) { $classes[] = 'single'; if ( isset( $post->post_type ) ) { $classes[] = 'single-' . sanitize_html_class( $post->post_type, $post_id ); $classes[] = 'postid-' . $post_id; // Post Format. if ( post_type_supports( $post->post_type, 'post-formats' ) ) { $post_format = get_post_format( $post->ID ); if ( $post_format && ! is_wp_error( $post_format ) ) { $classes[] = 'single-format-' . sanitize_html_class( $post_format ); } else { $classes[] = 'single-format-standard'; } } } } if ( is_attachment() ) { $mime_type = get_post_mime_type( $post_id ); $mime_prefix = array( 'application/', 'image/', 'text/', 'audio/', 'video/', 'music/' ); $classes[] = 'attachmentid-' . $post_id; $classes[] = 'attachment-' . str_replace( $mime_prefix, '', $mime_type ); } elseif ( is_page() ) { $classes[] = 'page'; $classes[] = 'page-id-' . $post_id; if ( get_pages( array( 'parent' => $post_id, 'number' => 1, ) ) ) { $classes[] = 'page-parent'; } if ( $post->post_parent ) { $classes[] = 'page-child'; $classes[] = 'parent-pageid-' . $post->post_parent; } } } elseif ( is_archive() ) { if ( is_post_type_archive() ) { $classes[] = 'post-type-archive'; $post_type = get_query_var( 'post_type' ); if ( is_array( $post_type ) ) { $post_type = reset( $post_type ); } $classes[] = 'post-type-archive-' . sanitize_html_class( $post_type ); } elseif ( is_author() ) { $author = $wp_query->get_queried_object(); $classes[] = 'author'; if ( isset( $author->user_nicename ) ) { $classes[] = 'author-' . sanitize_html_class( $author->user_nicename, $author->ID ); $classes[] = 'author-' . $author->ID; } } elseif ( is_category() ) { $cat = $wp_query->get_queried_object(); $classes[] = 'category'; if ( isset( $cat->term_id ) ) { $cat_class = sanitize_html_class( $cat->slug, $cat->term_id ); if ( is_numeric( $cat_class ) || ! trim( $cat_class, '-' ) ) { $cat_class = $cat->term_id; } $classes[] = 'category-' . $cat_class; $classes[] = 'category-' . $cat->term_id; } } elseif ( is_tag() ) { $tag = $wp_query->get_queried_object(); $classes[] = 'tag'; if ( isset( $tag->term_id ) ) { $tag_class = sanitize_html_class( $tag->slug, $tag->term_id ); if ( is_numeric( $tag_class ) || ! trim( $tag_class, '-' ) ) { $tag_class = $tag->term_id; } $classes[] = 'tag-' . $tag_class; $classes[] = 'tag-' . $tag->term_id; } } elseif ( is_tax() ) { $term = $wp_query->get_queried_object(); if ( isset( $term->term_id ) ) { $term_class = sanitize_html_class( $term->slug, $term->term_id ); if ( is_numeric( $term_class ) || ! trim( $term_class, '-' ) ) { $term_class = $term->term_id; } $classes[] = 'tax-' . sanitize_html_class( $term->taxonomy ); $classes[] = 'term-' . $term_class; $classes[] = 'term-' . $term->term_id; } } } if ( is_user_logged_in() ) { $classes[] = 'logged-in'; } if ( is_admin_bar_showing() ) { $classes[] = 'admin-bar'; $classes[] = 'no-customize-support'; } if ( current_theme_supports( 'custom-background' ) && ( get_background_color() !== get_theme_support( 'custom-background', 'default-color' ) || get_background_image() ) ) { $classes[] = 'custom-background'; } if ( has_custom_logo() ) { $classes[] = 'wp-custom-logo'; } if ( current_theme_supports( 'responsive-embeds' ) ) { $classes[] = 'wp-embed-responsive'; } $page = $wp_query->get( 'page' ); if ( ! $page || $page < 2 ) { $page = $wp_query->get( 'paged' ); } if ( $page && $page > 1 && ! is_404() ) { $classes[] = 'paged-' . $page; if ( is_single() ) { $classes[] = 'single-paged-' . $page; } elseif ( is_page() ) { $classes[] = 'page-paged-' . $page; } elseif ( is_category() ) { $classes[] = 'category-paged-' . $page; } elseif ( is_tag() ) { $classes[] = 'tag-paged-' . $page; } elseif ( is_date() ) { $classes[] = 'date-paged-' . $page; } elseif ( is_author() ) { $classes[] = 'author-paged-' . $page; } elseif ( is_search() ) { $classes[] = 'search-paged-' . $page; } elseif ( is_post_type_archive() ) { $classes[] = 'post-type-paged-' . $page; } } $classes[] = 'wp-theme-' . sanitize_html_class( get_template() ); if ( is_child_theme() ) { $classes[] = 'wp-child-theme-' . sanitize_html_class( get_stylesheet() ); } if ( ! empty( $css_class ) ) { if ( ! is_array( $css_class ) ) { $css_class = preg_split( '#\s+#', $css_class ); } $classes = array_merge( $classes, $css_class ); } else { // Ensure that we always coerce class to being an array. $css_class = array(); } $classes = array_map( 'esc_attr', $classes ); /** * Filters the list of CSS body class names for the current post or page. * * @since 2.8.0 * * @param string[] $classes An array of body class names. * @param string[] $css_class An array of additional class names added to the body. */ $classes = apply_filters( 'body_class', $classes, $css_class ); return array_unique( $classes ); } /** * Determines whether the post requires password and whether a correct password has been provided. * * @since 2.7.0 * * @param int|WP_Post|null $post An optional post. Global $post used if not provided. * @return bool false if a password is not required or the correct password cookie is present, true otherwise. */ function post_password_required( $post = null ) { $post = get_post( $post ); if ( empty( $post->post_password ) ) { /** This filter is documented in wp-includes/post-template.php */ return apply_filters( 'post_password_required', false, $post ); } if ( ! isset( $_COOKIE[ 'wp-postpass_' . COOKIEHASH ] ) ) { /** This filter is documented in wp-includes/post-template.php */ return apply_filters( 'post_password_required', true, $post ); } require_once ABSPATH . WPINC . '/class-phpass.php'; $hasher = new PasswordHash( 8, true ); $hash = wp_unslash( $_COOKIE[ 'wp-postpass_' . COOKIEHASH ] ); if ( ! str_starts_with( $hash, '$P$B' ) ) { $required = true; } else { $required = ! $hasher->CheckPassword( $post->post_password, $hash ); } /** * Filters whether a post requires the user to supply a password. * * @since 4.7.0 * * @param bool $required Whether the user needs to supply a password. True if password has not been * provided or is incorrect, false if password has been supplied or is not required. * @param WP_Post $post Post object. */ return apply_filters( 'post_password_required', $required, $post ); } // // Page Template Functions for usage in Themes. // /** * The formatted output of a list of pages. * * Displays page links for paginated posts (i.e. including the `` * Quicktag one or more times). This tag must be within The Loop. * * @since 1.2.0 * @since 5.1.0 Added the `aria_current` argument. * * @global int $page * @global int $numpages * @global int $multipage * @global int $more * * @param string|array $args { * Optional. Array or string of default arguments. * * @type string $before HTML or text to prepend to each link. Default is `
Pages:`. * @type string $after HTML or text to append to each link. Default is `
`. * @type string $link_before HTML or text to prepend to each link, inside the `` tag. * Also prepended to the current item, which is not linked. Default empty. * @type string $link_after HTML or text to append to each Pages link inside the `` tag. * Also appended to the current item, which is not linked. Default empty. * @type string $aria_current The value for the aria-current attribute. Possible values are 'page', * 'step', 'location', 'date', 'time', 'true', 'false'. Default is 'page'. * @type string $next_or_number Indicates whether page numbers should be used. Valid values are number * and next. Default is 'number'. * @type string $separator Text between pagination links. Default is ' '. * @type string $nextpagelink Link text for the next page link, if available. Default is 'Next Page'. * @type string $previouspagelink Link text for the previous page link, if available. Default is 'Previous Page'. * @type string $pagelink Format string for page numbers. The % in the parameter string will be * replaced with the page number, so 'Page %' generates "Page 1", "Page 2", etc. * Defaults to '%', just the page number. * @type int|bool $echo Whether to echo or not. Accepts 1|true or 0|false. Default 1|true. * } * @return string Formatted output in HTML. */ function wp_link_pages( $args = '' ) { global $page, $numpages, $multipage, $more; $defaults = array( 'before' => '', 'link_before' => '', 'link_after' => '', 'aria_current' => 'page', 'next_or_number' => 'number', 'separator' => ' ', 'nextpagelink' => __( 'Next page' ), 'previouspagelink' => __( 'Previous page' ), 'pagelink' => '%', 'echo' => 1, ); $parsed_args = wp_parse_args( $args, $defaults ); /** * Filters the arguments used in retrieving page links for paginated posts. * * @since 3.0.0 * * @param array $parsed_args An array of page link arguments. See wp_link_pages() * for information on accepted arguments. */ $parsed_args = apply_filters( 'wp_link_pages_args', $parsed_args ); $output = ''; if ( $multipage ) { if ( 'number' === $parsed_args['next_or_number'] ) { $output .= $parsed_args['before']; for ( $i = 1; $i <= $numpages; $i++ ) { $link = $parsed_args['link_before'] . str_replace( '%', $i, $parsed_args['pagelink'] ) . $parsed_args['link_after']; if ( $i !== $page || ! $more && 1 === $page ) { $link = _wp_link_page( $i ) . $link . ''; } elseif ( $i === $page ) { $link = '' . $link . ''; } /** * Filters the HTML output of individual page number links. * * @since 3.6.0 * * @param string $link The page number HTML output. * @param int $i Page number for paginated posts' page links. */ $link = apply_filters( 'wp_link_pages_link', $link, $i ); // Use the custom links separator beginning with the second link. $output .= ( 1 === $i ) ? ' ' : $parsed_args['separator']; $output .= $link; } $output .= $parsed_args['after']; } elseif ( $more ) { $output .= $parsed_args['before']; $prev = $page - 1; if ( $prev > 0 ) { $link = _wp_link_page( $prev ) . $parsed_args['link_before'] . $parsed_args['previouspagelink'] . $parsed_args['link_after'] . ''; /** This filter is documented in wp-includes/post-template.php */ $output .= apply_filters( 'wp_link_pages_link', $link, $prev ); } $next = $page + 1; if ( $next <= $numpages ) { if ( $prev ) { $output .= $parsed_args['separator']; } $link = _wp_link_page( $next ) . $parsed_args['link_before'] . $parsed_args['nextpagelink'] . $parsed_args['link_after'] . ''; /** This filter is documented in wp-includes/post-template.php */ $output .= apply_filters( 'wp_link_pages_link', $link, $next ); } $output .= $parsed_args['after']; } } /** * Filters the HTML output of page links for paginated posts. * * @since 3.6.0 * * @param string $output HTML output of paginated posts' page links. * @param array|string $args An array or query string of arguments. See wp_link_pages() * for information on accepted arguments. */ $html = apply_filters( 'wp_link_pages', $output, $args ); if ( $parsed_args['echo'] ) { echo $html; } return $html; } /** * Helper function for wp_link_pages(). * * @since 3.1.0 * @access private * * @global WP_Rewrite $wp_rewrite WordPress rewrite component. * * @param int $i Page number. * @return string Link. */ function _wp_link_page( $i ) { global $wp_rewrite; $post = get_post(); $query_args = array(); if ( 1 === $i ) { $url = get_permalink(); } else { if ( ! get_option( 'permalink_structure' ) || in_array( $post->post_status, array( 'draft', 'pending' ), true ) ) { $url = add_query_arg( 'page', $i, get_permalink() ); } elseif ( 'page' === get_option( 'show_on_front' ) && (int) get_option( 'page_on_front' ) === $post->ID ) { $url = trailingslashit( get_permalink() ) . user_trailingslashit( "$wp_rewrite->pagination_base/" . $i, 'single_paged' ); } else { $url = trailingslashit( get_permalink() ) . user_trailingslashit( $i, 'single_paged' ); } } if ( is_preview() ) { if ( ( 'draft' !== $post->post_status ) && isset( $_GET['preview_id'], $_GET['preview_nonce'] ) ) { $query_args['preview_id'] = wp_unslash( $_GET['preview_id'] ); $query_args['preview_nonce'] = wp_unslash( $_GET['preview_nonce'] ); } $url = get_preview_post_link( $post, $query_args, $url ); } return ''; } // // Post-meta: Custom per-post fields. // /** * Retrieves post custom meta data field. * * @since 1.5.0 * * @param string $key Meta data key name. * @return array|string|false Array of values, or single value if only one element exists. * False if the key does not exist. */ function post_custom( $key = '' ) { $custom = get_post_custom(); if ( ! isset( $custom[ $key ] ) ) { return false; } elseif ( 1 === count( $custom[ $key ] ) ) { return $custom[ $key ][0]; } else { return $custom[ $key ]; } } /** * Displays a list of post custom fields. * * @since 1.2.0 * * @deprecated 6.0.2 Use get_post_meta() to retrieve post meta and render manually. */ function the_meta() { _deprecated_function( __FUNCTION__, '6.0.2', 'get_post_meta()' ); $keys = get_post_custom_keys(); if ( $keys ) { $li_html = ''; foreach ( (array) $keys as $key ) { $keyt = trim( $key ); if ( is_protected_meta( $keyt, 'post' ) ) { continue; } $values = array_map( 'trim', get_post_custom_values( $key ) ); $value = implode( ', ', $values ); $html = sprintf( "' . $invalid_password . '
Content
<⃨!⃨-⃨-⃨ ⃨/⃨w⃨p⃨:⃨p⃨a⃨r⃨a⃨g⃨r⃨a⃨p⃨h⃨ ⃨-⃨-⃨>⃨ * │ ╰─ This delimiter was matched, and after matching, * │ revealed the preceding HTML span. * │ * ╰─ The first byte offset after the previous matched delimiter * is 21. Because the matched delimiter starts at 55, which is after * this, a span of HTML must exist between these boundaries. * * @since 6.9.0 * * @var int */ private $after_previous_delimiter = 0; /** * Byte offset where namespace span begins. * * When no namespace is present, this will be the same as the starting * byte offset for the block name. * * Example: * * * │ ╰─ Name starts here. * ╰─ Namespace starts here. * * * ├─ The namespace would start here but is implied as “core.” * ╰─ The name starts here. * * @since 6.9.0 * * @var int */ private $namespace_at = 0; /** * Byte offset where block name span begins. * * When no namespace is present, this will be the same as the starting * byte offset for the block namespace. * * Example: * * * │ ╰─ Name starts here. * ╰─ Namespace starts here. * * * ├─ The namespace would start here but is implied as “core.” * ╰─ The name starts here. * * @since 6.9.0 * * @var int */ private $name_at = 0; /** * Byte length of block name span. * * Example: * * 5 10 15 20 25 * * ╰─────╯ * 7 bytes long. * * @since 6.9.0 * * @var int */ private $name_length = 0; /** * Whether the delimiter contains the block-closing flag. * * This may be erroneous if present within a void block, * therefore the {@see self::has_closing_flag()} can be used by * calling code to perform custom error-handling. * * @since 6.9.0 * * @var bool */ private $has_closing_flag = false; /** * Byte offset where JSON attributes span begins. * * Example: * * 5 10 15 20 25 30 35 40 * * ╰─ Starts at byte offset 18. * * @since 6.9.0 * * @var int */ private $json_at; /** * Byte length of JSON attributes span, or 0 if none are present. * * Example: * * 5 10 15 20 25 30 35 40 * * ╰───────────────╯ * 17 bytes long. * * @since 6.9.0 * * @var int */ private $json_length = 0; /** * Internal parser state, differentiating whether the instance is currently matched, * on an implicit freeform node, in error, or ready to begin parsing. * * @see self::READY * @see self::MATCHED * @see self::HTML_SPAN * @see self::INCOMPLETE_INPUT * @see self::COMPLETE * * @since 6.9.0 * * @var string */ protected $state = self::READY; /** * Indicates what kind of block comment delimiter was matched. * * One of: * * - {@see self::OPENER} If the delimiter is opening a block. * - {@see self::CLOSER} If the delimiter is closing an open block. * - {@see self::VOID} If the delimiter represents a void block with no inner content. * * If a parsed comment delimiter contains both the closing and the void * flags then it will be interpreted as a void block to match the behavior * of the official block parser, however, this is a syntax error and probably * the block ought to close an open block of the same name, if one is open. * * @since 6.9.0 * * @var string */ private $type; /** * Whether the last-matched delimiter acts like a void block and should be * popped from the stack of open blocks as soon as the parser advances. * * This applies to void block delimiters and to HTML spans. * * @since 6.9.0 * * @var bool */ private $was_void = false; /** * For every open block, in hierarchical order, this stores the byte offset * into the source text where the block type starts, including for HTML spans. * * To avoid allocating and normalizing block names when they aren’t requested, * the stack of open blocks is stored as the byte offsets and byte lengths of * each open block’s block type. This allows for minimal tracking and quick * reading or comparison of block types when requested. * * @since 6.9.0 * * @see self::$open_blocks_length * * @var int[] */ private $open_blocks_at = array(); /** * For every open block, in hierarchical order, this stores the byte length * of the block’s block type in the source text. For HTML spans this is 0. * * @since 6.9.0 * * @see self::$open_blocks_at * * @var int[] */ private $open_blocks_length = array(); /** * Indicates which operation should apply to the stack of open blocks after * processing any pending spans of HTML. * * Since HTML spans are discovered after matching block delimiters, those * delimiters need to defer modifying the stack of open blocks. This value, * if set, indicates what operation should be applied. The properties * associated with token boundaries still point to the delimiters even * when processing HTML spans, so there’s no need to track them independently. * * @var 'push'|'void'|'pop'|null */ private $next_stack_op = null; /** * Creates a new block processor. * * Example: * * $processor = new WP_Block_Processor( $post_content ); * if ( $processor->next_block( 'core/image' ) ) { * echo "Found an image!\n"; * } * * @see self::next_block() to advance to the start of the next block (skips closers). * @see self::next_delimiter() to advance to the next explicit block delimiter. * @see self::next_token() to advance to the next block delimiter or HTML span. * * @since 6.9.0 * * @param string $source_text Input document potentially containing block content. */ public function __construct( string $source_text ) { $this->source_text = $source_text; } /** * Advance to the next block delimiter which opens a block, indicating if one was found. * * Delimiters which open blocks include opening and void block delimiters. To visit * freeform HTML content, pass the wildcard “*” as the block type. * * Use this function to walk through the blocks in a document, pausing where they open. * * Example blocks: * * // The first delimiter opens the paragraph block. * <⃨!⃨-⃨-⃨ ⃨w⃨p⃨:⃨p⃨a⃨r⃨a⃨g⃨r⃨a⃨p⃨h⃨ ⃨-⃨-⃨>⃨Content
* * // The void block is the first opener in this sequence of closers. * <⃨!⃨-⃨-⃨ ⃨w⃨p⃨:⃨s⃨p⃨a⃨c⃨e⃨r⃨ ⃨{⃨"⃨h⃨e⃨i⃨g⃨h⃨t⃨"⃨:⃨"⃨2⃨0⃨0⃨p⃨x⃨"⃨}⃨ ⃨/⃨-⃨-⃨>⃨ * * // If, however, `*` is provided as the block type, freeform content is matched. * <⃨h⃨2⃨>⃨M⃨y⃨ ⃨s⃨y⃨n⃨o⃨p⃨s⃨i⃨s⃨<⃨/⃨h⃨2⃨>⃨\⃨n⃨ * * // Inner HTML is never freeform content, and will not be matched even with the wildcard. * <⃨!⃨-⃨-⃨ ⃨w⃨p⃨:⃨p⃨a⃨r⃨a⃨g⃨r⃨a⃨p⃨h⃨ ⃨-⃨>⃨* * Example: * * // Find all textual ranges of image block opening delimiters. * $images = array(); * $processor = new WP_Block_Processor( $html ); * while ( $processor->next_block( 'core/image' ) ) { * $images[] = $processor->get_span(); * } * * In some cases it may be useful to conditionally visit the implicit freeform * blocks, such as when determining if a post contains freeform content that * isn’t purely whitespace. * * Example: * * $seen_block_types = []; * $block_type = '*'; * $processor = new WP_Block_Processor( $html ); * while ( $processor->next_block( $block_type ) { * // Stop wasting time visiting freeform blocks after one has been found. * if ( * '*' === $block_type && * null === $processor->get_block_type() && * $processor->is_non_whitespace_html() * ) { * $block_type = null; * $seen_block_types['core/freeform'] = true; * continue; * } * * $seen_block_types[ $processor->get_block_type() ] = true; * } * * @since 6.9.0 * * @see self::next_delimiter() to advance to the next explicit block delimiter. * @see self::next_token() to advance to the next block delimiter or HTML span. * * @param string|null $block_type Optional. If provided, advance until a block of this type is found. * Default is to stop at any block regardless of its type. * @return bool Whether an opening delimiter for a block was found. */ public function next_block( ?string $block_type = null ): bool { while ( $this->next_delimiter( $block_type ) ) { if ( self::CLOSER !== $this->get_delimiter_type() ) { return true; } } return false; } /** * Advance to the next block delimiter in a document, indicating if one was found. * * Delimiters may include invalid JSON. This parser does not attempt to parse the * JSON attributes until requested; when invalid, the attributes will be null. This * matches the behavior of {@see \parse_blocks()}. To visit freeform HTML content, * pass the wildcard “*” as the block type. * * Use this function to walk through the block delimiters in a document. * * Example delimiters: * * * * * * // If the wildcard `*` is provided as the block type, freeform content is matched. * <⃨h⃨2⃨>⃨M⃨y⃨ ⃨s⃨y⃨n⃨o⃨p⃨s⃨i⃨s⃨<⃨/⃨h⃨2⃨>⃨\⃨n⃨ * * // Inner HTML is never freeform content, and will not be matched even with the wildcard. * ...<⃨!⃨-⃨-⃨ ⃨/⃨w⃨p⃨:⃨l⃨i⃨s⃨t⃨ ⃨-⃨-⃨>⃨
* * Example: * * $html = '\n'; * $processor = new WP_Block_Processor( $html ); * while ( $processor->next_delimiter() { * // Runs twice, seeing both void blocks of type “core/void.” * } * * $processor = new WP_Block_Processor( $html ); * while ( $processor->next_delimiter( '*' ) ) { * // Runs thrice, seeing the void block, the newline span, and the void block. * } * * @since 6.9.0 * * @param string|null $block_name Optional. Keep searching until a block of this name is found. * Defaults to visit every block regardless of type. * @return bool Whether a block delimiter was matched. */ public function next_delimiter( ?string $block_name = null ): bool { if ( ! isset( $block_name ) ) { while ( $this->next_token() ) { if ( ! $this->is_html() ) { return true; } } return false; } while ( $this->next_token() ) { if ( $this->is_block_type( $block_name ) ) { return true; } } return false; } /** * Advance to the next block delimiter or HTML span in a document, indicating if one was found. * * This function steps through every syntactic chunk in a document. This includes explicit * block comment delimiters, freeform non-block content, and inner HTML segments. * * Example tokens: * * * * *
Normal HTML content
* Plaintext content too! * * Example: * * // Find span containing wrapping HTML element surrounding inner blocks. * $processor = new WP_Block_Processor( $html ); * if ( ! $processor->next_block( 'gallery' ) ) { * return null; * } * * $containing_span = null; * while ( $processor->next_token() && $processor->is_html() ) { * $containing_span = $processor->get_span(); * } * * This method will visit all HTML spans including those forming freeform non-block * content as well as those which are part of a block’s inner HTML. * * @since 6.9.0 * * @return bool Whether a token was matched or the end of the document was reached without finding any. */ public function next_token(): bool { if ( $this->last_error || self::COMPLETE === $this->state || self::INCOMPLETE_INPUT === $this->state ) { return false; } // Void tokens automatically pop off the stack of open blocks. if ( $this->was_void ) { array_pop( $this->open_blocks_at ); array_pop( $this->open_blocks_length ); $this->was_void = false; } $text = $this->source_text; $end = strlen( $text ); /* * Because HTML spans are inferred after finding the next delimiter, it means that * the parser must transition out of that HTML state and reuse the token boundaries * it found after the HTML span. If those boundaries are before the end of the * document it implies that a real delimiter was found; otherwise this must be the * terminating HTML span and the parsing is complete. */ if ( self::HTML_SPAN === $this->state ) { if ( $this->matched_delimiter_at >= $end ) { $this->state = self::COMPLETE; return false; } switch ( $this->next_stack_op ) { case 'void': $this->was_void = true; $this->open_blocks_at[] = $this->namespace_at; $this->open_blocks_length[] = $this->name_at + $this->name_length - $this->namespace_at; break; case 'push': $this->open_blocks_at[] = $this->namespace_at; $this->open_blocks_length[] = $this->name_at + $this->name_length - $this->namespace_at; break; case 'pop': array_pop( $this->open_blocks_at ); array_pop( $this->open_blocks_length ); break; } $this->next_stack_op = null; $this->state = self::MATCHED; return true; } $this->state = self::READY; $after_prev_delimiter = $this->matched_delimiter_at + $this->matched_delimiter_length; $at = $after_prev_delimiter; while ( $at < $end ) { /* * Find the next possible start of a delimiter. * * This follows the behavior in the official block parser, which segments a post * by the block comment delimiters. It is possible for an HTML attribute to contain * what looks like a block comment delimiter but which is actually an HTML attribute * value. In such a case, the parser here will break apart the HTML and create the * block boundary inside the HTML attribute. In other words, the block parser * isolates sections of HTML from each other, even if that leads to malformed markup. * * For a more robust parse, scan through the document with the HTML API and parse * comments once they are matched to see if they are also block delimiters. In * practice, this nuance has not caused any known problems since developing blocks. * * <⃨!⃨-⃨-⃨ /wp:core/paragraph {"dropCap":true} /--> */ $comment_opening_at = strpos( $text, ' $opening_whitespace_at = $comment_opening_at + 4; if ( $opening_whitespace_at >= $end ) { goto incomplete; } $opening_whitespace_length = strspn( $text, " \t\f\r\n", $opening_whitespace_at ); /* * The `wp` prefix cannot come before this point, but it may come after it * depending on the presence of the closer. This is detected next. */ $wp_prefix_at = $opening_whitespace_at + $opening_whitespace_length; if ( $wp_prefix_at >= $end ) { goto incomplete; } if ( 0 === $opening_whitespace_length ) { $at = $this->find_html_comment_end( $comment_opening_at, $end ); continue; } // $has_closer = false; if ( '/' === $text[ $wp_prefix_at ] ) { $has_closer = true; ++$wp_prefix_at; } // if ( $wp_prefix_at < $end && 0 !== substr_compare( $text, 'wp:', $wp_prefix_at, 3 ) ) { if ( ( $wp_prefix_at + 2 >= $end && str_ends_with( $text, 'wp' ) ) || ( $wp_prefix_at + 1 >= $end && str_ends_with( $text, 'w' ) ) ) { goto incomplete; } $at = $this->find_html_comment_end( $comment_opening_at, $end ); continue; } /* * If the block contains no namespace, this will end up masquerading with * the block name. It’s easier to first detect the span and then determine * if it’s a namespace of a name. * * */ $namespace_at = $wp_prefix_at + 3; if ( $namespace_at >= $end ) { goto incomplete; } $start_of_namespace = $text[ $namespace_at ]; // The namespace must start with a-z. if ( 'a' > $start_of_namespace || 'z' < $start_of_namespace ) { $at = $this->find_html_comment_end( $comment_opening_at, $end ); continue; } $namespace_length = 1 + strspn( $text, 'abcdefghijklmnopqrstuvwxyz0123456789-_', $namespace_at + 1 ); $separator_at = $namespace_at + $namespace_length; if ( $separator_at >= $end ) { goto incomplete; } // $has_separator = '/' === $text[ $separator_at ]; if ( $has_separator ) { $name_at = $separator_at + 1; if ( $name_at >= $end ) { goto incomplete; } // $start_of_name = $text[ $name_at ]; if ( 'a' > $start_of_name || 'z' < $start_of_name ) { $at = $this->find_html_comment_end( $comment_opening_at, $end ); continue; } $name_length = 1 + strspn( $text, 'abcdefghijklmnopqrstuvwxyz0123456789-_', $name_at + 1 ); } else { $name_at = $namespace_at; $name_length = $namespace_length; } if ( $name_at + $name_length >= $end ) { goto incomplete; } /* * For this next section of the delimiter, it could be the JSON attributes * or it could be the end of the comment. Assume that the JSON is there and * update if it’s not. */ // $after_name_whitespace_at = $name_at + $name_length; $after_name_whitespace_length = strspn( $text, " \t\f\r\n", $after_name_whitespace_at ); $json_at = $after_name_whitespace_at + $after_name_whitespace_length; if ( $json_at >= $end ) { goto incomplete; } if ( 0 === $after_name_whitespace_length ) { $at = $this->find_html_comment_end( $comment_opening_at, $end ); continue; } // $has_json = '{' === $text[ $json_at ]; $json_length = 0; /* * For the final span of the delimiter it's most efficient to find the end of the * HTML comment and work backwards. This prevents complicated parsing inside the * JSON span, which is not allowed to contain the HTML comment terminator. * * This also matches the behavior in the official block parser, * even though it allows for matching invalid JSON content. * * ', $json_at ); if ( false === $comment_closing_at ) { goto incomplete; } // if ( '/' === $text[ $comment_closing_at - 1 ] ) { $has_void_flag = true; $void_flag_length = 1; } else { $has_void_flag = false; $void_flag_length = 0; } /* * If there's no JSON, then the span of text after the name * until the comment closing must be completely whitespace. * Otherwise it’s a normal HTML comment. */ if ( ! $has_json ) { if ( $after_name_whitespace_at + $after_name_whitespace_length === $comment_closing_at - $void_flag_length ) { // This must be a block delimiter! $this->state = self::MATCHED; break; } $at = $this->find_html_comment_end( $comment_opening_at, $end ); continue; } /* * There's JSON, so attempt to find its boundary. * * @todo It’s likely faster to scan forward instead of in reverse. * * */ $after_json_whitespace_length = 0; for ( $char_at = $comment_closing_at - $void_flag_length - 1; $char_at > $json_at; $char_at-- ) { $char = $text[ $char_at ]; switch ( $char ) { case ' ': case "\t": case "\f": case "\r": case "\n": ++$after_json_whitespace_length; continue 2; case '}': $json_length = $char_at - $json_at + 1; break 2; default: ++$at; continue 3; } } /* * This covers cases where there is no terminating “}” or where * mandatory whitespace is missing. */ if ( 0 === $json_length || 0 === $after_json_whitespace_length ) { $at = $this->find_html_comment_end( $comment_opening_at, $end ); continue; } // This must be a block delimiter! $this->state = self::MATCHED; break; } // The end of the document was reached without a match. if ( self::MATCHED !== $this->state ) { $this->state = self::COMPLETE; return false; } /* * From this point forward, a delimiter has been matched. There * might also be an HTML span that appears before the delimiter. */ $this->after_previous_delimiter = $after_prev_delimiter; $this->matched_delimiter_at = $comment_opening_at; $this->matched_delimiter_length = $comment_closing_at + 3 - $comment_opening_at; $this->namespace_at = $namespace_at; $this->name_at = $name_at; $this->name_length = $name_length; $this->json_at = $json_at; $this->json_length = $json_length; /* * When delimiters contain both the void flag and the closing flag * they shall be interpreted as void blocks, per the spec parser. */ if ( $has_void_flag ) { $this->type = self::VOID; $this->next_stack_op = 'void'; } elseif ( $has_closer ) { $this->type = self::CLOSER; $this->next_stack_op = 'pop'; /* * @todo Check if the name matches and bail according to the spec parser. * The default parser doesn’t examine the names. */ } else { $this->type = self::OPENER; $this->next_stack_op = 'push'; } $this->has_closing_flag = $has_closer; // HTML spans are visited before the delimiter that follows them. if ( $comment_opening_at > $after_prev_delimiter ) { $this->state = self::HTML_SPAN; $this->open_blocks_at[] = $after_prev_delimiter; $this->open_blocks_length[] = 0; $this->was_void = true; return true; } // If there were no HTML spans then flush the enqueued stack operations immediately. switch ( $this->next_stack_op ) { case 'void': $this->was_void = true; $this->open_blocks_at[] = $namespace_at; $this->open_blocks_length[] = $name_at + $name_length - $namespace_at; break; case 'push': $this->open_blocks_at[] = $namespace_at; $this->open_blocks_length[] = $name_at + $name_length - $namespace_at; break; case 'pop': array_pop( $this->open_blocks_at ); array_pop( $this->open_blocks_length ); break; } $this->next_stack_op = null; return true; incomplete: $this->state = self::COMPLETE; $this->last_error = self::INCOMPLETE_INPUT; return false; } /** * Returns an array containing the names of the currently-open blocks, in order * from outermost to innermost, with HTML spans indicated as “#html”. * * Example: * * // Freeform HTML content is an HTML span. * $processor = new WP_Block_Processor( 'Just text' ); * $processor->next_token(); * array( '#text' ) === $processor->get_breadcrumbs(); * * $processor = new WP_Block_Processor( '' ); * $processor->next_token(); * array( 'core/a' ) === $processor->get_breadcrumbs(); * $processor->next_token(); * array( 'core/a', 'core/b' ) === $processor->get_breadcrumbs(); * $processor->next_token(); * // Void blocks are only open while visiting them. * array( 'core/a', 'core/b', 'core/c' ) === $processor->get_breadcrumbs(); * $processor->next_token(); * // Blocks are closed before visiting their closing delimiter. * array( 'core/a' ) === $processor->get_breadcrumbs(); * $processor->next_token(); * array() === $processor->get_breadcrumbs(); * * // Inner HTML is also an HTML span. * $processor = new WP_Block_Processor( 'Inner HTML' ); * $processor->next_token(); * $processor->next_token(); * array( 'core/a', '#html' ) === $processor->get_breadcrumbs(); * * @since 6.9.0 * * @return string[] */ public function get_breadcrumbs(): array { $breadcrumbs = array_fill( 0, count( $this->open_blocks_at ), null ); /* * Since HTML spans can only be at the very end, set the normalized block name for * each open element and then work backwards after creating the array. This allows * for the elimination of a conditional on each iteration of the loop. */ foreach ( $this->open_blocks_at as $i => $at ) { $block_type = substr( $this->source_text, $at, $this->open_blocks_length[ $i ] ); $breadcrumbs[ $i ] = self::normalize_block_type( $block_type ); } if ( isset( $i ) && 0 === $this->open_blocks_length[ $i ] ) { $breadcrumbs[ $i ] = '#html'; } return $breadcrumbs; } /** * Returns the depth of the open blocks where the processor is currently matched. * * Depth increases before visiting openers and void blocks and decreases before * visiting closers. HTML spans behave like void blocks. * * @since 6.9.0 * * @return int */ public function get_depth(): int { return count( $this->open_blocks_at ); } /** * Extracts a block object, and all inner content, starting at a matched opening * block delimiter, or at a matched top-level HTML span as freeform HTML content. * * Use this function to extract some blocks within a document, but not all. For example, * one might want to find image galleries, parse them, modify them, and then reserialize * them in place. * * Once this function returns, the parser will be matched on token following the close * of the given block. * * The return type of this method is compatible with the return of {@see \parse_blocks()}. * * Example: * * $processor = new WP_Block_Processor( $post_content ); * if ( ! $processor->next_block( 'gallery' ) ) { * return $post_content; * } * * $gallery_at = $processor->get_span()->start; * $gallery = $processor->extract_full_block_and_advance(); * $ends_before = $processor->get_span(); * $ends_before = $ends_before->start ?? strlen( $post_content ); * * $new_gallery = update_gallery( $gallery ); * $new_gallery = serialize_block( $new_gallery ); * * return ( * substr( $post_content, 0, $gallery_at ) . * $new_gallery . * substr( $post_content, $ends_before ) * ); * * @since 6.9.0 * * @return array[]|null { * Array of block structures. * * @type array ...$0 { * An associative array of a single parsed block object. See WP_Block_Parser_Block. * * @type string|null $blockName Name of block. * @type array $attrs Attributes from block comment delimiters. * @type array[] $innerBlocks List of inner blocks. An array of arrays that * have the same structure as this one. * @type string $innerHTML HTML from inside block comment delimiters. * @type array $innerContent List of string fragments and null markers where * inner blocks were found. * } * } */ public function extract_full_block_and_advance(): ?array { if ( $this->is_html() ) { $chunk = $this->get_html_content(); return array( 'blockName' => null, 'attrs' => array(), 'innerBlocks' => array(), 'innerHTML' => $chunk, 'innerContent' => array( $chunk ), ); } $block = array( 'blockName' => $this->get_block_type(), 'attrs' => $this->allocate_and_return_parsed_attributes() ?? array(), 'innerBlocks' => array(), 'innerHTML' => '', 'innerContent' => array(), ); $depth = $this->get_depth(); while ( $this->next_token() && $this->get_depth() > $depth ) { if ( $this->is_html() ) { $chunk = $this->get_html_content(); $block['innerHTML'] .= $chunk; $block['innerContent'][] = $chunk; continue; } /** * Inner blocks. * * @todo This is a decent place to call {@link \render_block()} * @todo Use iteration instead of recursion, or at least refactor to tail-call form. */ if ( $this->opens_block() ) { $inner_block = $this->extract_full_block_and_advance(); $block['innerBlocks'][] = $inner_block; $block['innerContent'][] = null; } /* * Because the parser has advanced past the closing block token, it * may be matched on an HTML span. This needs to be processed before * moving on to the next token at the start of the next loop iteration. */ if ( $this->is_html() ) { $chunk = $this->get_html_content(); $block['innerHTML'] .= $chunk; $block['innerContent'][] = $chunk; } } return $block; } /** * Returns the byte-offset after the ending character of an HTML comment, * assuming the proper starting byte offset. * * @since 6.9.0 * * @param int $comment_starting_at Where the HTML comment started, the leading `<`. * @param int $search_end Last offset in which to search, for limiting search span. * @return int Offset after the current HTML comment ends, or `$search_end` if no end was found. */ private function find_html_comment_end( int $comment_starting_at, int $search_end ): int { $text = $this->source_text; // Find span-of-dashes comments which look like ``. $span_of_dashes = strspn( $text, '-', $comment_starting_at + 2 ); if ( $comment_starting_at + 2 + $span_of_dashes < $search_end && '>' === $text[ $comment_starting_at + 2 + $span_of_dashes ] ) { return $comment_starting_at + $span_of_dashes + 1; } // Otherwise, there are other characters inside the comment, find the first `-->` or `--!>`. $now_at = $comment_starting_at + 4; while ( $now_at < $search_end ) { $dashes_at = strpos( $text, '--', $now_at ); if ( false === $dashes_at ) { return $search_end; } $closer_must_be_at = $dashes_at + 2 + strspn( $text, '-', $dashes_at + 2 ); if ( $closer_must_be_at < $search_end && '!' === $text[ $closer_must_be_at ] ) { ++$closer_must_be_at; } if ( $closer_must_be_at < $search_end && '>' === $text[ $closer_must_be_at ] ) { return $closer_must_be_at + 1; } ++$now_at; } return $search_end; } /** * Indicates if the last attempt to parse a block comment delimiter * failed, if set, otherwise `null` if the last attempt succeeded. * * @since 6.9.0 * * @return string|null Error from last attempt at parsing next block delimiter, * or `null` if last attempt succeeded. */ public function get_last_error(): ?string { return $this->last_error; } /** * Indicates if the last attempt to parse a block’s JSON attributes failed. * * @see \json_last_error() * * @since 6.9.0 * * @return int JSON_ERROR_ code from last attempt to parse block JSON attributes. */ public function get_last_json_error(): int { return $this->last_json_error; } /** * Returns the type of the block comment delimiter. * * One of: * * - {@see self::OPENER} * - {@see self::CLOSER} * - {@see self::VOID} * - `null` * * @since 6.9.0 * * @return string|null type of the block comment delimiter, if currently matched. */ public function get_delimiter_type(): ?string { switch ( $this->state ) { case self::HTML_SPAN: return self::VOID; case self::MATCHED: return $this->type; default: return null; } } /** * Returns whether the delimiter contains the closing flag. * * This should be avoided except in cases of custom error-handling * with block closers containing the void flag. For normative use, * {@see self::get_delimiter_type()}. * * @since 6.9.0 * * @return bool Whether the currently-matched block delimiter contains the closing flag. */ public function has_closing_flag(): bool { return $this->has_closing_flag; } /** * Indicates if the block delimiter represents a block of the given type. * * Since the “core” namespace may be implicit, it’s allowable to pass * either the fully-qualified block type with namespace and block name * as well as the shorthand version only containing the block name, if * the desired block is in the “core” namespace. * * Since freeform HTML content is non-block content, it has no block type. * Passing the wildcard “*” will, however, return true for all block types, * even the implicit freeform content, though not for spans of inner HTML. * * Example: * * $is_core_paragraph = $processor->is_block_type( 'paragraph' ); * $is_core_paragraph = $processor->is_block_type( 'core/paragraph' ); * $is_formula = $processor->is_block_type( 'math-block/formula' ); * * @param string $block_type Block type name for the desired block. * E.g. "paragraph", "core/paragraph", "math-blocks/formula". * @return bool Whether this delimiter represents a block of the given type. */ public function is_block_type( string $block_type ): bool { if ( '*' === $block_type ) { return true; } if ( $this->is_html() ) { // This is a core/freeform text block, it’s special. if ( 0 === ( $this->open_blocks_length[0] ?? null ) ) { return ( 'core/freeform' === $block_type || 'freeform' === $block_type ); } // Otherwise this is innerHTML and not a block. return false; } return $this->are_equal_block_types( $this->source_text, $this->namespace_at, $this->name_at - $this->namespace_at + $this->name_length, $block_type, 0, strlen( $block_type ) ); } /** * Given two spans of text, indicate if they represent identical block types. * * This function normalizes block types to account for implicit core namespacing. * * Note! This function only returns valid results when the complete block types are * represented in the span offsets and lengths. This means that the full optional * namespace and block name must be represented in the input arguments. * * Example: * * 0 5 10 15 20 25 30 35 40 * $text = ''; * * true === WP_Block_Processor::are_equal_block_types( $text, 9, 5, $text, 27, 10 ); * false === WP_Block_Processor::are_equal_block_types( $text, 9, 5, 'my/block', 0, 8 ); * * @since 6.9.0 * * @param string $a_text Text in which first block type appears. * @param int $a_at Byte offset into text in which first block type starts. * @param int $a_length Byte length of first block type. * @param string $b_text Text in which second block type appears (may be the same as the first text). * @param int $b_at Byte offset into text in which second block type starts. * @param int $b_length Byte length of second block type. * @return bool Whether the spans of text represent identical block types, normalized for namespacing. */ public static function are_equal_block_types( string $a_text, int $a_at, int $a_length, string $b_text, int $b_at, int $b_length ): bool { $a_ns_length = strcspn( $a_text, '/', $a_at, $a_length ); $b_ns_length = strcspn( $b_text, '/', $b_at, $b_length ); $a_has_ns = $a_ns_length !== $a_length; $b_has_ns = $b_ns_length !== $b_length; // Both contain namespaces. if ( $a_has_ns && $b_has_ns ) { if ( $a_length !== $b_length ) { return false; } $a_block_type = substr( $a_text, $a_at, $a_length ); return 0 === substr_compare( $b_text, $a_block_type, $b_at, $b_length ); } if ( $a_has_ns ) { $b_block_type = 'core/' . substr( $b_text, $b_at, $b_length ); return ( strlen( $b_block_type ) === $a_length && 0 === substr_compare( $a_text, $b_block_type, $a_at, $a_length ) ); } if ( $b_has_ns ) { $a_block_type = 'core/' . substr( $a_text, $a_at, $a_length ); return ( strlen( $a_block_type ) === $b_length && 0 === substr_compare( $b_text, $a_block_type, $b_at, $b_length ) ); } // Neither contains a namespace. if ( $a_length !== $b_length ) { return false; } $a_name = substr( $a_text, $a_at, $a_length ); return 0 === substr_compare( $b_text, $a_name, $b_at, $b_length ); } /** * Indicates if the matched delimiter is an opening or void delimiter of the given type, * if a type is provided, otherwise if it opens any block or implicit freeform HTML content. * * This is a helper method to ease handling of code inspecting where blocks start, and for * checking if the blocks are of a given type. The function is variadic to allow for * checking if the delimiter opens one of many possible block types. * * To advance to the start of a block {@see self::next_block()}. * * Example: * * $processor = new WP_Block_Processor( $html ); * while ( $processor->next_delimiter() ) { * if ( $processor->opens_block( 'core/code', 'syntaxhighlighter/code' ) ) { * echo "Found code!"; * continue; * } * * if ( $processor->opens_block( 'core/image' ) ) { * echo "Found an image!"; * continue; * } * * if ( $processor->opens_block() ) { * echo "Found a new block!"; * } * } * * @since 6.9.0 * * @see self::is_block_type() * * @param string[] $block_type Optional. Is the matched block type one of these? * If none are provided, will not test block type. * @return bool Whether the matched block delimiter opens a block, and whether it * opens a block of one of the given block types, if provided. */ public function opens_block( string ...$block_type ): bool { // HTML spans only open implicit freeform content at the top level. if ( self::HTML_SPAN === $this->state && 1 !== count( $this->open_blocks_at ) ) { return false; } /* * Because HTML spans are discovered after the next delimiter is found, * the delimiter type when visiting HTML spans refers to the type of the * following delimiter. Therefore the HTML case is handled by checking * the state and depth of the stack of open block. */ if ( self::CLOSER === $this->type && ! $this->is_html() ) { return false; } if ( count( $block_type ) === 0 ) { return true; } foreach ( $block_type as $block ) { if ( $this->is_block_type( $block ) ) { return true; } } return false; } /** * Indicates if the matched delimiter is an HTML span. * * @since 6.9.0 * * @see self::is_non_whitespace_html() * * @return bool Whether the processor is matched on an HTML span. */ public function is_html(): bool { return self::HTML_SPAN === $this->state; } /** * Indicates if the matched delimiter is an HTML span and comprises more * than whitespace characters, i.e. contains real content. * * Many block serializers introduce newlines between block delimiters, * so the presence of top-level non-block content does not imply that * there are “real” freeform HTML blocks. Checking if there is content * beyond whitespace is a more certain check, such as for determining * whether to load CSS for the freeform or fallback block type. * * @since 6.9.0 * * @see self::is_html() * * @return bool Whether the currently-matched delimiter is an HTML * span containing non-whitespace text. */ public function is_non_whitespace_html(): bool { if ( ! $this->is_html() ) { return false; } $length = $this->matched_delimiter_at - $this->after_previous_delimiter; $whitespace_length = strspn( $this->source_text, " \t\f\r\n", $this->after_previous_delimiter, $length ); return $whitespace_length !== $length; } /** * Returns the string content of a matched HTML span, or `null` otherwise. * * @since 6.9.0 * * @return string|null Raw HTML content, or `null` if not currently matched on HTML. */ public function get_html_content(): ?string { if ( ! $this->is_html() ) { return null; } return substr( $this->source_text, $this->after_previous_delimiter, $this->matched_delimiter_at - $this->after_previous_delimiter ); } /** * Allocates a substring for the block type and returns the fully-qualified * name, including the namespace, if matched on a delimiter, otherwise `null`. * * This function is like {@see self::get_printable_block_type()} but when * paused on a freeform HTML block, will return `null` instead of “core/freeform”. * The `null` behavior matches what {@see \parse_blocks()} returns but may not * be as useful as having a string value. * * This function allocates a substring for the given block type. This * allocation will be small and likely fine in most cases, but it's * preferable to call {@see self::is_block_type()} if only needing * to know whether the delimiter is for a given block type, as that * function is more efficient for this purpose and avoids the allocation. * * Example: * * // Avoid. * 'core/paragraph' = $processor->get_block_type(); * * // Prefer. * $processor->is_block_type( 'core/paragraph' ); * $processor->is_block_type( 'paragraph' ); * $processor->is_block_type( 'core/freeform' ); * * // Freeform HTML content has no block type. * $processor = new WP_Block_Processor( 'non-block content' ); * $processor->next_token(); * null === $processor->get_block_type(); * * @since 6.9.0 * * @see self::are_equal_block_types() * * @return string|null Fully-qualified block namespace and type, e.g. "core/paragraph", * if matched on an explicit delimiter, otherwise `null`. */ public function get_block_type(): ?string { if ( self::READY === $this->state || self::COMPLETE === $this->state || self::INCOMPLETE_INPUT === $this->state ) { return null; } // This is a core/freeform text block, it’s special. if ( $this->is_html() ) { return null; } $block_type = substr( $this->source_text, $this->namespace_at, $this->name_at - $this->namespace_at + $this->name_length ); return self::normalize_block_type( $block_type ); } /** * Allocates a printable substring for the block type and returns the fully-qualified * name, including the namespace, if matched on a delimiter or freeform block, otherwise `null`. * * This function is like {@see self::get_block_type()} but when paused on a freeform * HTML block, will return “core/freeform” instead of `null`. The `null` behavior matches * what {@see \parse_blocks()} returns but may not be as useful as having a string value. * * This function allocates a substring for the given block type. This * allocation will be small and likely fine in most cases, but it's * preferable to call {@see self::is_block_type()} if only needing * to know whether the delimiter is for a given block type, as that * function is more efficient for this purpose and avoids the allocation. * * Example: * * // Avoid. * 'core/paragraph' = $processor->get_printable_block_type(); * * // Prefer. * $processor->is_block_type( 'core/paragraph' ); * $processor->is_block_type( 'paragraph' ); * $processor->is_block_type( 'core/freeform' ); * * // Freeform HTML content is given an implicit type. * $processor = new WP_Block_Processor( 'non-block content' ); * $processor->next_token(); * 'core/freeform' === $processor->get_printable_block_type(); * * @since 6.9.0 * * @see self::are_equal_block_types() * * @return string|null Fully-qualified block namespace and type, e.g. "core/paragraph", * if matched on an explicit delimiter or freeform block, otherwise `null`. */ public function get_printable_block_type(): ?string { if ( self::READY === $this->state || self::COMPLETE === $this->state || self::INCOMPLETE_INPUT === $this->state ) { return null; } // This is a core/freeform text block, it’s special. if ( $this->is_html() ) { return 1 === count( $this->open_blocks_at ) ? 'core/freeform' : '#innerHTML'; } $block_type = substr( $this->source_text, $this->namespace_at, $this->name_at - $this->namespace_at + $this->name_length ); return self::normalize_block_type( $block_type ); } /** * Normalizes a block name to ensure that missing implicit “core” namespaces are present. * * Example: * * 'core/paragraph' === WP_Block_Processor::normalize_block_byte( 'paragraph' ); * 'core/paragraph' === WP_Block_Processor::normalize_block_byte( 'core/paragraph' ); * 'my/paragraph' === WP_Block_Processor::normalize_block_byte( 'my/paragraph' ); * * @since 6.9.0 * * @param string $block_type Valid block name, potentially without a namespace. * @return string Fully-qualified block type including namespace. */ public static function normalize_block_type( string $block_type ): string { return false === strpos( $block_type, '/' ) ? "core/{$block_type}" : $block_type; } /** * Returns a lazy wrapper around the block attributes, which can be used * for efficiently interacting with the JSON attributes. * * This stub hints that there should be a lazy interface for parsing * block attributes but doesn’t define it. It serves both as a placeholder * for one to come as well as a guard against implementing an eager * function in its place. * * @throws Exception This function is a stub for subclasses to implement * when providing streaming attribute parsing. * * @since 6.9.0 * * @see self::allocate_and_return_parsed_attributes() * * @return never */ public function get_attributes() { throw new Exception( 'Lazy attribute parsing not yet supported' ); } /** * Attempts to parse and return the entire JSON attributes from the delimiter, * allocating memory and processing the JSON span in the process. * * This does not return any parsed attributes for a closing block delimiter * even if there is a span of JSON content; this JSON is a parsing error. * * Consider calling {@see static::get_attributes()} instead if it's not * necessary to read all the attributes at the same time, as that provides * a more efficient mechanism for typical use cases. * * Since the JSON span inside the comment delimiter may not be valid JSON, * this function will return `null` if it cannot parse the span and set the * {@see static::get_last_json_error()} to the appropriate JSON_ERROR_ constant. * * If the delimiter contains no JSON span, it will also return `null`, * but the last error will be set to {@see \JSON_ERROR_NONE}. * * Example: * * $processor = new WP_Block_Processor( '' ); * $processor->next_delimiter(); * $memory_hungry_and_slow_attributes = $processor->allocate_and_return_parsed_attributes(); * $memory_hungry_and_slow_attributes === array( 'url' => 'https://wordpress.org/favicon.ico' ); * * $processor = new WP_Block_Processor( '' ); * $processor->next_delimiter(); * null = $processor->allocate_and_return_parsed_attributes(); * JSON_ERROR_NONE = $processor->get_last_json_error(); * * $processor = new WP_Block_Processor( '' ); * $processor->next_delimiter(); * array() === $processor->allocate_and_return_parsed_attributes(); * * $processor = new WP_Block_Processor( '' ); * $processor->next_delimiter(); * null = $processor->allocate_and_return_parsed_attributes(); * * $processor = new WP_Block_Processor( '' ); * $processor->next_delimiter(); * null = $processor->allocate_and_return_parsed_attributes(); * JSON_ERROR_CTRL_CHAR = $processor->get_last_json_error(); * * @since 6.9.0 * * @return array|null Parsed JSON attributes, if present and valid, otherwise `null`. */ public function allocate_and_return_parsed_attributes(): ?array { $this->last_json_error = JSON_ERROR_NONE; if ( self::CLOSER === $this->type || $this->is_html() || 0 === $this->json_length ) { return null; } $json_span = substr( $this->source_text, $this->json_at, $this->json_length ); $parsed = json_decode( $json_span, null, 512, JSON_OBJECT_AS_ARRAY | JSON_INVALID_UTF8_SUBSTITUTE ); $last_error = json_last_error(); $this->last_json_error = $last_error; return ( JSON_ERROR_NONE === $last_error && is_array( $parsed ) ) ? $parsed : null; } /** * Returns the span representing the currently-matched delimiter, if matched, otherwise `null`. * * Example: * * $processor = new WP_Block_Processor( '' ); * null === $processor->get_span(); * * $processor->next_delimiter(); * WP_HTML_Span( 0, 17 ) === $processor->get_span(); * * @since 6.9.0 * * @return WP_HTML_Span|null Span of text in source text spanning matched delimiter. */ public function get_span(): ?WP_HTML_Span { switch ( $this->state ) { case self::HTML_SPAN: return new WP_HTML_Span( $this->after_previous_delimiter, $this->matched_delimiter_at - $this->after_previous_delimiter ); case self::MATCHED: return new WP_HTML_Span( $this->matched_delimiter_at, $this->matched_delimiter_length ); default: return null; } } // // Constant declarations that would otherwise pollute the top of the class. // /** * Indicates that the block comment delimiter closes an open block. * * @see self::$type * * @since 6.9.0 */ const CLOSER = 'closer'; /** * Indicates that the block comment delimiter opens a block. * * @see self::$type * * @since 6.9.0 */ const OPENER = 'opener'; /** * Indicates that the block comment delimiter represents a void block * with no inner content of any kind. * * @see self::$type * * @since 6.9.0 */ const VOID = 'void'; /** * Indicates that the processor is ready to start parsing but hasn’t yet begun. * * @see self::$state * * @since 6.9.0 */ const READY = 'processor-ready'; /** * Indicates that the processor is matched on an explicit block delimiter. * * @see self::$state * * @since 6.9.0 */ const MATCHED = 'processor-matched'; /** * Indicates that the processor is matched on the opening of an implicit freeform delimiter. * * @see self::$state * * @since 6.9.0 */ const HTML_SPAN = 'processor-html-span'; /** * Indicates that the parser started parsing a block comment delimiter, but * the input document ended before it could finish. The document was likely truncated. * * @see self::$state * * @since 6.9.0 */ const INCOMPLETE_INPUT = 'incomplete-input'; /** * Indicates that the processor has finished parsing and has nothing left to scan. * * @see self::$state * * @since 6.9.0 */ const COMPLETE = 'processor-complete'; } '', 'container' => 'div', 'container_class' => '', 'container_id' => '', 'container_aria_label' => '', 'menu_class' => 'menu', 'menu_id' => '', 'echo' => true, 'fallback_cb' => 'wp_page_menu', 'before' => '', 'after' => '', 'link_before' => '', 'link_after' => '', 'items_wrap' => 'type => link',
'taxonomy => link_category'
)
);
$args['taxonomy'] = 'link_category';
}
$categories = get_terms( $args );
if ( is_wp_error( $categories ) ) {
$categories = array();
} else {
$categories = (array) $categories;
foreach ( array_keys( $categories ) as $k ) {
_make_cat_compat( $categories[ $k ] );
}
}
return $categories;
}
/**
* Retrieves category data given a category ID or category object.
*
* If you pass the $category parameter an object, which is assumed to be the
* category row object retrieved the database. It will cache the category data.
*
* If you pass $category an integer of the category ID, then that category will
* be retrieved from the database, if it isn't already cached, and pass it back.
*
* If you look at get_term(), then both types will be passed through several
* filters and finally sanitized based on the $filter parameter value.
*
* @since 1.5.1
*
* @param int|object $category Category ID or category row object.
* @param string $output Optional. The required return type. One of OBJECT, ARRAY_A, or ARRAY_N, which
* correspond to a WP_Term object, an associative array, or a numeric array,
* respectively. Default OBJECT.
* @param string $filter Optional. How to sanitize category fields. Default 'raw'.
* @return WP_Term|array|WP_Error|null Category data in type defined by $output parameter.
* Returns a WP_Term object with backwards compatible property aliases filled in.
* WP_Error if $category is empty, null if it does not exist.
*/
function get_category( $category, $output = OBJECT, $filter = 'raw' ) {
$category = get_term( $category, 'category', $output, $filter );
if ( is_wp_error( $category ) ) {
return $category;
}
_make_cat_compat( $category );
return $category;
}
/**
* Retrieves a category based on URL containing the category slug.
*
* Breaks the $category_path parameter up to get the category slug.
*
* Tries to find the child path and will return it. If it doesn't find a
* match, then it will return the first category matching slug, if $full_match,
* is set to false. If it does not, then it will return null.
*
* It is also possible that it will return a WP_Error object on failure. Check
* for it when using this function.
*
* @since 2.1.0
*
* @param string $category_path URL containing category slugs.
* @param bool $full_match Optional. Whether full path should be matched.
* @param string $output Optional. The required return type. One of OBJECT, ARRAY_A, or ARRAY_N, which
* correspond to a WP_Term object, an associative array, or a numeric array,
* respectively. Default OBJECT.
* @return WP_Term|array|WP_Error|null Type is based on $output value.
*/
function get_category_by_path( $category_path, $full_match = true, $output = OBJECT ) {
$category_path = rawurlencode( urldecode( $category_path ) );
$category_path = str_replace( '%2F', '/', $category_path );
$category_path = str_replace( '%20', ' ', $category_path );
$category_paths = '/' . trim( $category_path, '/' );
$leaf_path = sanitize_title( basename( $category_paths ) );
$category_paths = explode( '/', $category_paths );
$full_path = '';
foreach ( (array) $category_paths as $pathdir ) {
$full_path .= ( '' !== $pathdir ? '/' : '' ) . sanitize_title( $pathdir );
}
$categories = get_terms(
array(
'taxonomy' => 'category',
'get' => 'all',
'slug' => $leaf_path,
)
);
if ( empty( $categories ) ) {
return;
}
foreach ( $categories as $category ) {
$path = '/' . $leaf_path;
$curcategory = $category;
while ( ( 0 !== $curcategory->parent ) && ( $curcategory->parent !== $curcategory->term_id ) ) {
$curcategory = get_term( $curcategory->parent, 'category' );
if ( is_wp_error( $curcategory ) ) {
return $curcategory;
}
$path = '/' . $curcategory->slug . $path;
}
if ( $path === $full_path ) {
$category = get_term( $category->term_id, 'category', $output );
_make_cat_compat( $category );
return $category;
}
}
// If full matching is not required, return the first cat that matches the leaf.
if ( ! $full_match ) {
$category = get_term( reset( $categories )->term_id, 'category', $output );
_make_cat_compat( $category );
return $category;
}
}
/**
* Retrieves a category object by category slug.
*
* @since 2.3.0
*
* @param string $slug The category slug.
* @return object|false Category data object on success, false if not found.
*/
function get_category_by_slug( $slug ) {
$category = get_term_by( 'slug', $slug, 'category' );
if ( $category ) {
_make_cat_compat( $category );
}
return $category;
}
/**
* Retrieves the ID of a category from its name.
*
* @since 1.0.0
*
* @param string $cat_name Category name.
* @return int Category ID on success, 0 if the category doesn't exist.
*/
function get_cat_ID( $cat_name ) { // phpcs:ignore WordPress.NamingConventions.ValidFunctionName.FunctionNameInvalid
$cat = get_term_by( 'name', $cat_name, 'category' );
if ( $cat ) {
return $cat->term_id;
}
return 0;
}
/**
* Retrieves the name of a category from its ID.
*
* @since 1.0.0
*
* @param int $cat_id Category ID.
* @return string Category name, or an empty string if the category doesn't exist.
*/
function get_cat_name( $cat_id ) {
$cat_id = (int) $cat_id;
$category = get_term( $cat_id, 'category' );
if ( ! $category || is_wp_error( $category ) ) {
return '';
}
return $category->name;
}
/**
* Checks if a category is an ancestor of another category.
*
* You can use either an ID or the category object for both parameters.
* If you use an integer, the category will be retrieved.
*
* @since 2.1.0
*
* @param int|object $cat1 ID or object to check if this is the parent category.
* @param int|object $cat2 The child category.
* @return bool Whether $cat2 is child of $cat1.
*/
function cat_is_ancestor_of( $cat1, $cat2 ) {
return term_is_ancestor_of( $cat1, $cat2, 'category' );
}
/**
* Sanitizes category data based on context.
*
* @since 2.3.0
*
* @param object|array $category Category data.
* @param string $context Optional. Default 'display'.
* @return object|array Same type as $category with sanitized data for safe use.
*/
function sanitize_category( $category, $context = 'display' ) {
return sanitize_term( $category, 'category', $context );
}
/**
* Sanitizes data in single category key field.
*
* @since 2.3.0
*
* @param string $field Category key to sanitize.
* @param mixed $value Category value to sanitize.
* @param int $cat_id Category ID.
* @param string $context What filter to use, 'raw', 'display', etc.
* @return mixed Value after $value has been sanitized.
*/
function sanitize_category_field( $field, $value, $cat_id, $context ) {
return sanitize_term_field( $field, $value, $cat_id, 'category', $context );
}
/* Tags */
/**
* Retrieves all post tags.
*
* @since 2.3.0
*
* @param string|array $args {
* Optional. Arguments to retrieve tags. See get_terms() for additional options.
*
* @type string $taxonomy Taxonomy to retrieve terms for. Default 'post_tag'.
* }
* @return WP_Term[]|int|WP_Error Array of 'post_tag' term objects, a count thereof,
* or WP_Error if any of the taxonomies do not exist.
*/
function get_tags( $args = '' ) {
$defaults = array( 'taxonomy' => 'post_tag' );
$args = wp_parse_args( $args, $defaults );
$tags = get_terms( $args );
if ( empty( $tags ) ) {
$tags = array();
} else {
/**
* Filters the array of term objects returned for the 'post_tag' taxonomy.
*
* @since 2.3.0
*
* @param WP_Term[]|int|WP_Error $tags Array of 'post_tag' term objects, a count thereof,
* or WP_Error if any of the taxonomies do not exist.
* @param array $args An array of arguments. See {@see get_terms()}.
*/
$tags = apply_filters( 'get_tags', $tags, $args );
}
return $tags;
}
/**
* Retrieves a post tag by tag ID or tag object.
*
* If you pass the $tag parameter an object, which is assumed to be the tag row
* object retrieved from the database, it will cache the tag data.
*
* If you pass $tag an integer of the tag ID, then that tag will be retrieved
* from the database, if it isn't already cached, and passed back.
*
* If you look at get_term(), both types will be passed through several filters
* and finally sanitized based on the $filter parameter value.
*
* @since 2.3.0
*
* @param int|WP_Term|object $tag A tag ID or object.
* @param string $output Optional. The required return type. One of OBJECT, ARRAY_A, or ARRAY_N, which
* correspond to a WP_Term object, an associative array, or a numeric array,
* respectively. Default OBJECT.
* @param string $filter Optional. How to sanitize tag fields. Default 'raw'.
* @return WP_Term|array|WP_Error|null Tag data in type defined by $output parameter.
* WP_Error if $tag is empty, null if it does not exist.
*/
function get_tag( $tag, $output = OBJECT, $filter = 'raw' ) {
return get_term( $tag, 'post_tag', $output, $filter );
}
/* Cache */
/**
* Removes the category cache data based on ID.
*
* @since 2.1.0
*
* @param int $id Category ID
*/
function clean_category_cache( $id ) {
clean_term_cache( $id, 'category' );
}
/**
* Updates category structure to old pre-2.3 from new taxonomy structure.
*
* This function was added for the taxonomy support to update the new category
* structure with the old category one. This will maintain compatibility with
* plugins and themes which depend on the old key or property names.
*
* The parameter should only be passed a variable and not create the array or
* object inline to the parameter. The reason for this is that parameter is
* passed by reference and PHP will fail unless it has the variable.
*
* There is no return value, because everything is updated on the variable you
* pass to it. This is one of the features with using pass by reference in PHP.
*
* @since 2.3.0
* @since 4.4.0 The `$category` parameter now also accepts a WP_Term object.
* @access private
*
* @param array|object|WP_Term $category Category row object or array.
*/
function _make_cat_compat( &$category ) {
if ( is_object( $category ) && ! is_wp_error( $category ) ) {
$category->cat_ID = $category->term_id;
$category->category_count = $category->count;
$category->category_description = $category->description;
$category->cat_name = $category->name;
$category->category_nicename = $category->slug;
$category->category_parent = $category->parent;
} elseif ( is_array( $category ) && isset( $category['term_id'] ) ) {
$category['cat_ID'] = &$category['term_id'];
$category['category_count'] = &$category['count'];
$category['category_description'] = &$category['description'];
$category['cat_name'] = &$category['name'];
$category['category_nicename'] = &$category['slug'];
$category['category_parent'] = &$category['parent'];
}
}
getTimestamp() + $datetime->getOffset();
}
if ( $translate ) {
return wp_date( $format, $datetime->getTimestamp(), $timezone );
}
return $datetime->format( $format );
}
/**
* Retrieves the current time based on specified type.
*
* - The 'mysql' type will return the time in the format for MySQL DATETIME field.
* - The 'timestamp' or 'U' types will return the current timestamp or a sum of timestamp
* and timezone offset, depending on `$gmt`.
* - Other strings will be interpreted as PHP date formats (e.g. 'Y-m-d').
*
* If `$gmt` is a truthy value then both types will use GMT time, otherwise the
* output is adjusted with the GMT offset for the site.
*
* @since 1.0.0
* @since 5.3.0 Now returns an integer if `$type` is 'U'. Previously a string was returned.
*
* @param string $type Type of time to retrieve. Accepts 'mysql', 'timestamp', 'U',
* or PHP date format string (e.g. 'Y-m-d').
* @param bool $gmt Optional. Whether to use GMT timezone. Default false.
* @return int|string Integer if `$type` is 'timestamp' or 'U', string otherwise.
*/
function current_time( $type, $gmt = false ) {
// Don't use non-GMT timestamp, unless you know the difference and really need to.
if ( 'timestamp' === $type || 'U' === $type ) {
return $gmt ? time() : time() + (int) ( (float) get_option( 'gmt_offset' ) * HOUR_IN_SECONDS );
}
if ( 'mysql' === $type ) {
$type = 'Y-m-d H:i:s';
}
$timezone = $gmt ? new DateTimeZone( 'UTC' ) : wp_timezone();
$datetime = new DateTime( 'now', $timezone );
return $datetime->format( $type );
}
/**
* Retrieves the current time as an object using the site's timezone.
*
* @since 5.3.0
*
* @return DateTimeImmutable Date and time object.
*/
function current_datetime() {
return new DateTimeImmutable( 'now', wp_timezone() );
}
/**
* Retrieves the timezone of the site as a string.
*
* Uses the `timezone_string` option to get a proper timezone name if available,
* otherwise falls back to a manual UTC ± offset.
*
* Example return values:
*
* - 'Europe/Rome'
* - 'America/North_Dakota/New_Salem'
* - 'UTC'
* - '-06:30'
* - '+00:00'
* - '+08:45'
*
* @since 5.3.0
*
* @return string PHP timezone name or a ±HH:MM offset.
*/
function wp_timezone_string() {
$timezone_string = get_option( 'timezone_string' );
if ( $timezone_string ) {
return $timezone_string;
}
$offset = (float) get_option( 'gmt_offset' );
$hours = (int) $offset;
$minutes = ( $offset - $hours );
$sign = ( $offset < 0 ) ? '-' : '+';
$abs_hour = abs( $hours );
$abs_mins = abs( $minutes * 60 );
$tz_offset = sprintf( '%s%02d:%02d', $sign, $abs_hour, $abs_mins );
return $tz_offset;
}
/**
* Retrieves the timezone of the site as a `DateTimeZone` object.
*
* Timezone can be based on a PHP timezone string or a ±HH:MM offset.
*
* @since 5.3.0
*
* @return DateTimeZone Timezone object.
*/
function wp_timezone() {
return new DateTimeZone( wp_timezone_string() );
}
/**
* Retrieves the date in localized format, based on a sum of Unix timestamp and
* timezone offset in seconds.
*
* If the locale specifies the locale month and weekday, then the locale will
* take over the format for the date. If it isn't, then the date format string
* will be used instead.
*
* Note that due to the way WP typically generates a sum of timestamp and offset
* with `strtotime()`, it implies offset added at a _current_ time, not at the time
* the timestamp represents. Storing such timestamps or calculating them differently
* will lead to invalid output.
*
* @since 0.71
* @since 5.3.0 Converted into a wrapper for wp_date().
*
* @param string $format Format to display the date.
* @param int|bool $timestamp_with_offset Optional. A sum of Unix timestamp and timezone offset
* in seconds. Default false.
* @param bool $gmt Optional. Whether to use GMT timezone. Only applies
* if timestamp is not provided. Default false.
* @return string The date, translated if locale specifies it.
*/
function date_i18n( $format, $timestamp_with_offset = false, $gmt = false ) {
$timestamp = $timestamp_with_offset;
// If timestamp is omitted it should be current time (summed with offset, unless `$gmt` is true).
if ( ! is_numeric( $timestamp ) ) {
// phpcs:ignore WordPress.DateTime.CurrentTimeTimestamp.Requested
$timestamp = current_time( 'timestamp', $gmt );
}
/*
* This is a legacy implementation quirk that the returned timestamp is also with offset.
* Ideally this function should never be used to produce a timestamp.
*/
if ( 'U' === $format ) {
$date = $timestamp;
} elseif ( $gmt && false === $timestamp_with_offset ) { // Current time in UTC.
$date = wp_date( $format, null, new DateTimeZone( 'UTC' ) );
} elseif ( false === $timestamp_with_offset ) { // Current time in site's timezone.
$date = wp_date( $format );
} else {
/*
* Timestamp with offset is typically produced by a UTC `strtotime()` call on an input without timezone.
* This is the best attempt to reverse that operation into a local time to use.
*/
$local_time = gmdate( 'Y-m-d H:i:s', $timestamp );
$timezone = wp_timezone();
$datetime = date_create( $local_time, $timezone );
$date = wp_date( $format, $datetime->getTimestamp(), $timezone );
}
/**
* Filters the date formatted based on the locale.
*
* @since 2.8.0
*
* @param string $date Formatted date string.
* @param string $format Format to display the date.
* @param int $timestamp A sum of Unix timestamp and timezone offset in seconds.
* Might be without offset if input omitted timestamp but requested GMT.
* @param bool $gmt Whether to use GMT timezone. Only applies if timestamp was not provided.
*/
$date = apply_filters( 'date_i18n', $date, $format, $timestamp, $gmt );
return $date;
}
/**
* Retrieves the date, in localized format.
*
* This is a newer function, intended to replace `date_i18n()` without legacy quirks in it.
*
* Note that, unlike `date_i18n()`, this function accepts a true Unix timestamp, not summed
* with timezone offset.
*
* @since 5.3.0
*
* @global WP_Locale $wp_locale WordPress date and time locale object.
*
* @param string $format PHP date format.
* @param int|null $timestamp Optional. Unix timestamp. Defaults to current time.
* @param DateTimeZone|null $timezone Optional. Timezone to output result in. Defaults to timezone
* from site settings.
* @return string|false The date, translated if locale specifies it. False on invalid timestamp input.
*/
function wp_date( $format, $timestamp = null, $timezone = null ) {
global $wp_locale;
if ( null === $timestamp ) {
$timestamp = time();
} elseif ( ! is_numeric( $timestamp ) ) {
return false;
}
if ( ! $timezone ) {
$timezone = wp_timezone();
}
$datetime = date_create( '@' . $timestamp );
$datetime->setTimezone( $timezone );
if ( empty( $wp_locale->month ) || empty( $wp_locale->weekday ) ) {
$date = $datetime->format( $format );
} else {
// We need to unpack shorthand `r` format because it has parts that might be localized.
$format = preg_replace( '/(?get_month( $datetime->format( 'm' ) );
$weekday = $wp_locale->get_weekday( $datetime->format( 'w' ) );
for ( $i = 0; $i < $format_length; $i++ ) {
switch ( $format[ $i ] ) {
case 'D':
$new_format .= addcslashes( $wp_locale->get_weekday_abbrev( $weekday ), '\\A..Za..z' );
break;
case 'F':
$new_format .= addcslashes( $month, '\\A..Za..z' );
break;
case 'l':
$new_format .= addcslashes( $weekday, '\\A..Za..z' );
break;
case 'M':
$new_format .= addcslashes( $wp_locale->get_month_abbrev( $month ), '\\A..Za..z' );
break;
case 'a':
$new_format .= addcslashes( $wp_locale->get_meridiem( $datetime->format( 'a' ) ), '\\A..Za..z' );
break;
case 'A':
$new_format .= addcslashes( $wp_locale->get_meridiem( $datetime->format( 'A' ) ), '\\A..Za..z' );
break;
case '\\':
$new_format .= $format[ $i ];
// If character follows a slash, we add it without translating.
if ( $i < $format_length ) {
$new_format .= $format[ ++$i ];
}
break;
default:
$new_format .= $format[ $i ];
break;
}
}
$date = $datetime->format( $new_format );
$date = wp_maybe_decline_date( $date, $format );
}
/**
* Filters the date formatted based on the locale.
*
* @since 5.3.0
*
* @param string $date Formatted date string.
* @param string $format Format to display the date.
* @param int $timestamp Unix timestamp.
* @param DateTimeZone $timezone Timezone.
*/
$date = apply_filters( 'wp_date', $date, $format, $timestamp, $timezone );
return $date;
}
/**
* Determines if the date should be declined.
*
* If the locale specifies that month names require a genitive case in certain
* formats (like 'j F Y'), the month name will be replaced with a correct form.
*
* @since 4.4.0
* @since 5.4.0 The `$format` parameter was added.
*
* @global WP_Locale $wp_locale WordPress date and time locale object.
*
* @param string $date Formatted date string.
* @param string $format Optional. Date format to check. Default empty string.
* @return string The date, declined if locale specifies it.
*/
function wp_maybe_decline_date( $date, $format = '' ) {
global $wp_locale;
// i18n functions are not available in SHORTINIT mode.
if ( ! function_exists( '_x' ) ) {
return $date;
}
/*
* translators: If months in your language require a genitive case,
* translate this to 'on'. Do not translate into your own language.
*/
if ( 'on' === _x( 'off', 'decline months names: on or off' ) ) {
$months = $wp_locale->month;
$months_genitive = $wp_locale->month_genitive;
/*
* Match a format like 'j F Y' or 'j. F' (day of the month, followed by month name)
* and decline the month.
*/
if ( $format ) {
$decline = preg_match( '#[dj]\.? F#', $format );
} else {
// If the format is not passed, try to guess it from the date string.
$decline = preg_match( '#\b\d{1,2}\.? [^\d ]+\b#u', $date );
}
if ( $decline ) {
foreach ( $months as $key => $month ) {
$months[ $key ] = '# ' . preg_quote( $month, '#' ) . '\b#u';
}
foreach ( $months_genitive as $key => $month ) {
$months_genitive[ $key ] = ' ' . $month;
}
$date = preg_replace( $months, $months_genitive, $date );
}
/*
* Match a format like 'F jS' or 'F j' (month name, followed by day with an optional ordinal suffix)
* and change it to declined 'j F'.
*/
if ( $format ) {
$decline = preg_match( '#F [dj]#', $format );
} else {
// If the format is not passed, try to guess it from the date string.
$decline = preg_match( '#\b[^\d ]+ \d{1,2}(st|nd|rd|th)?\b#u', trim( $date ) );
}
if ( $decline ) {
foreach ( $months as $key => $month ) {
$months[ $key ] = '#\b' . preg_quote( $month, '#' ) . ' (\d{1,2})(st|nd|rd|th)?([-–]\d{1,2})?(st|nd|rd|th)?\b#u';
}
foreach ( $months_genitive as $key => $month ) {
$months_genitive[ $key ] = '$1$3 ' . $month;
}
$date = preg_replace( $months, $months_genitive, $date );
}
}
// Used for locale-specific rules.
$locale = get_locale();
if ( 'ca' === $locale ) {
// " de abril| de agost| de octubre..." -> " d'abril| d'agost| d'octubre..."
$date = preg_replace( '# de ([ao])#i', " d'\\1", $date );
}
return $date;
}
/**
* Converts float number to format based on the locale.
*
* @since 2.3.0
*
* @global WP_Locale $wp_locale WordPress date and time locale object.
*
* @param float $number The number to convert based on locale.
* @param int $decimals Optional. Precision of the number of decimal places. Default 0.
* @return string Converted number in string format.
*/
function number_format_i18n( $number, $decimals = 0 ) {
global $wp_locale;
if ( isset( $wp_locale ) ) {
$formatted = number_format( $number, absint( $decimals ), $wp_locale->number_format['decimal_point'], $wp_locale->number_format['thousands_sep'] );
} else {
$formatted = number_format( $number, absint( $decimals ) );
}
/**
* Filters the number formatted based on the locale.
*
* @since 2.8.0
* @since 4.9.0 The `$number` and `$decimals` parameters were added.
*
* @param string $formatted Converted number in string format.
* @param float $number The number to convert based on locale.
* @param int $decimals Precision of the number of decimal places.
*/
return apply_filters( 'number_format_i18n', $formatted, $number, $decimals );
}
/**
* Converts a number of bytes to the largest unit the bytes will fit into.
*
* It is easier to read 1 KB than 1024 bytes and 1 MB than 1048576 bytes. Converts
* number of bytes to human readable number by taking the number of that unit
* that the bytes will go into it. Supports YB value.
*
* Please note that integers in PHP are limited to 32 bits, unless they are on
* 64 bit architecture, then they have 64 bit size. If you need to place the
* larger size then what PHP integer type will hold, then use a string. It will
* be converted to a double, which should always have 64 bit length.
*
* Technically the correct unit names for powers of 1024 are KiB, MiB etc.
*
* @since 2.3.0
* @since 6.0.0 Support for PB, EB, ZB, and YB was added.
*
* @param int|string $bytes Number of bytes. Note max integer size for integers.
* @param int $decimals Optional. Precision of number of decimal places. Default 0.
* @return string|false Number string on success, false on failure.
*/
function size_format( $bytes, $decimals = 0 ) {
$quant = array(
/* translators: Unit symbol for yottabyte. */
_x( 'YB', 'unit symbol' ) => YB_IN_BYTES,
/* translators: Unit symbol for zettabyte. */
_x( 'ZB', 'unit symbol' ) => ZB_IN_BYTES,
/* translators: Unit symbol for exabyte. */
_x( 'EB', 'unit symbol' ) => EB_IN_BYTES,
/* translators: Unit symbol for petabyte. */
_x( 'PB', 'unit symbol' ) => PB_IN_BYTES,
/* translators: Unit symbol for terabyte. */
_x( 'TB', 'unit symbol' ) => TB_IN_BYTES,
/* translators: Unit symbol for gigabyte. */
_x( 'GB', 'unit symbol' ) => GB_IN_BYTES,
/* translators: Unit symbol for megabyte. */
_x( 'MB', 'unit symbol' ) => MB_IN_BYTES,
/* translators: Unit symbol for kilobyte. */
_x( 'KB', 'unit symbol' ) => KB_IN_BYTES,
/* translators: Unit symbol for byte. */
_x( 'B', 'unit symbol' ) => 1,
);
if ( 0 === $bytes ) {
/* translators: Unit symbol for byte. */
return number_format_i18n( 0, $decimals ) . ' ' . _x( 'B', 'unit symbol' );
}
foreach ( $quant as $unit => $mag ) {
if ( (float) $bytes >= $mag ) {
return number_format_i18n( $bytes / $mag, $decimals ) . ' ' . $unit;
}
}
return false;
}
/**
* Converts a duration to human readable format.
*
* @since 5.1.0
*
* @param string $duration Duration will be in string format (HH:ii:ss) OR (ii:ss),
* with a possible prepended negative sign (-).
* @return string|false A human readable duration string, false on failure.
*/
function human_readable_duration( $duration = '' ) {
if ( ( empty( $duration ) || ! is_string( $duration ) ) ) {
return false;
}
$duration = trim( $duration );
// Remove prepended negative sign.
if ( str_starts_with( $duration, '-' ) ) {
$duration = substr( $duration, 1 );
}
// Extract duration parts.
$duration_parts = array_reverse( explode( ':', $duration ) );
$duration_count = count( $duration_parts );
$hour = null;
$minute = null;
$second = null;
if ( 3 === $duration_count ) {
// Validate HH:ii:ss duration format.
if ( ! ( (bool) preg_match( '/^([0-9]+):([0-5]?[0-9]):([0-5]?[0-9])$/', $duration ) ) ) {
return false;
}
// Three parts: hours, minutes & seconds.
list( $second, $minute, $hour ) = $duration_parts;
} elseif ( 2 === $duration_count ) {
// Validate ii:ss duration format.
if ( ! ( (bool) preg_match( '/^([0-5]?[0-9]):([0-5]?[0-9])$/', $duration ) ) ) {
return false;
}
// Two parts: minutes & seconds.
list( $second, $minute ) = $duration_parts;
} else {
return false;
}
$human_readable_duration = array();
// Add the hour part to the string.
if ( is_numeric( $hour ) ) {
/* translators: %s: Time duration in hour or hours. */
$human_readable_duration[] = sprintf( _n( '%s hour', '%s hours', $hour ), (int) $hour );
}
// Add the minute part to the string.
if ( is_numeric( $minute ) ) {
/* translators: %s: Time duration in minute or minutes. */
$human_readable_duration[] = sprintf( _n( '%s minute', '%s minutes', $minute ), (int) $minute );
}
// Add the second part to the string.
if ( is_numeric( $second ) ) {
/* translators: %s: Time duration in second or seconds. */
$human_readable_duration[] = sprintf( _n( '%s second', '%s seconds', $second ), (int) $second );
}
return implode( ', ', $human_readable_duration );
}
/**
* Gets the week start and end from the datetime or date string from MySQL.
*
* @since 0.71
*
* @param string $mysqlstring Date or datetime field type from MySQL.
* @param int|string $start_of_week Optional. Start of the week as an integer. Default empty string.
* @return int[] {
* Week start and end dates as Unix timestamps.
*
* @type int $start The week start date as a Unix timestamp.
* @type int $end The week end date as a Unix timestamp.
* }
*/
function get_weekstartend( $mysqlstring, $start_of_week = '' ) {
// MySQL string year.
$my = substr( $mysqlstring, 0, 4 );
// MySQL string month.
$mm = substr( $mysqlstring, 8, 2 );
// MySQL string day.
$md = substr( $mysqlstring, 5, 2 );
// The timestamp for MySQL string day.
$day = mktime( 0, 0, 0, $md, $mm, $my );
// The day of the week from the timestamp.
$weekday = (int) gmdate( 'w', $day );
if ( ! is_numeric( $start_of_week ) ) {
$start_of_week = (int) get_option( 'start_of_week' );
}
if ( $weekday < $start_of_week ) {
$weekday += 7;
}
// The most recent week start day on or before $day.
$start = $day - DAY_IN_SECONDS * ( $weekday - $start_of_week );
// $start + 1 week - 1 second.
$end = $start + WEEK_IN_SECONDS - 1;
return compact( 'start', 'end' );
}
/**
* Serializes data, if needed.
*
* @since 2.0.5
*
* @param string|array|object $data Data that might be serialized.
* @return mixed A scalar data.
*/
function maybe_serialize( $data ) {
if ( is_array( $data ) || is_object( $data ) ) {
return serialize( $data );
}
/*
* Double serialization is required for backward compatibility.
* See https://core.trac.wordpress.org/ticket/12930
* Also the world will end. See WP 3.6.1.
*/
if ( is_serialized( $data, false ) ) {
return serialize( $data );
}
return $data;
}
/**
* Unserializes data only if it was serialized.
*
* @since 2.0.0
*
* @param string $data Data that might be unserialized.
* @return mixed Unserialized data can be any type.
*/
function maybe_unserialize( $data ) {
if ( is_serialized( $data ) ) { // Don't attempt to unserialize data that wasn't serialized going in.
return @unserialize( trim( $data ) );
}
return $data;
}
/**
* Checks value to find if it was serialized.
*
* If $data is not a string, then returned value will always be false.
* Serialized data is always a string.
*
* @since 2.0.5
* @since 6.1.0 Added Enum support.
*
* @param string $data Value to check to see if was serialized.
* @param bool $strict Optional. Whether to be strict about the end of the string. Default true.
* @return bool False if not serialized and true if it was.
*/
function is_serialized( $data, $strict = true ) {
// If it isn't a string, it isn't serialized.
if ( ! is_string( $data ) ) {
return false;
}
$data = trim( $data );
if ( 'N;' === $data ) {
return true;
}
if ( strlen( $data ) < 4 ) {
return false;
}
if ( ':' !== $data[1] ) {
return false;
}
if ( $strict ) {
$lastc = substr( $data, -1 );
if ( ';' !== $lastc && '}' !== $lastc ) {
return false;
}
} else {
$semicolon = strpos( $data, ';' );
$brace = strpos( $data, '}' );
// Either ; or } must exist.
if ( false === $semicolon && false === $brace ) {
return false;
}
// But neither must be in the first X characters.
if ( false !== $semicolon && $semicolon < 3 ) {
return false;
}
if ( false !== $brace && $brace < 4 ) {
return false;
}
}
$token = $data[0];
switch ( $token ) {
case 's':
if ( $strict ) {
if ( '"' !== substr( $data, -2, 1 ) ) {
return false;
}
} elseif ( ! str_contains( $data, '"' ) ) {
return false;
}
// Or else fall through.
case 'a':
case 'O':
case 'E':
return (bool) preg_match( "/^{$token}:[0-9]+:/s", $data );
case 'b':
case 'i':
case 'd':
$end = $strict ? '$' : '';
return (bool) preg_match( "/^{$token}:[0-9.E+-]+;$end/", $data );
}
return false;
}
/**
* Checks whether serialized data is of string type.
*
* @since 2.0.5
*
* @param string $data Serialized data.
* @return bool False if not a serialized string, true if it is.
*/
function is_serialized_string( $data ) {
// if it isn't a string, it isn't a serialized string.
if ( ! is_string( $data ) ) {
return false;
}
$data = trim( $data );
if ( strlen( $data ) < 4 ) {
return false;
} elseif ( ':' !== $data[1] ) {
return false;
} elseif ( ! str_ends_with( $data, ';' ) ) {
return false;
} elseif ( 's' !== $data[0] ) {
return false;
} elseif ( '"' !== substr( $data, -2, 1 ) ) {
return false;
} else {
return true;
}
}
/**
* Retrieves post title from XML-RPC XML.
*
* If the `title` element is not found in the XML, the default post title
* from the `$post_default_title` global will be used instead.
*
* @since 0.71
*
* @global string $post_default_title Default XML-RPC post title.
*
* @param string $content XML-RPC XML Request content.
* @return string Post title.
*/
function xmlrpc_getposttitle( $content ) {
global $post_default_title;
if ( preg_match( '/'; $html .= sprintf( /* translators: %s: Logout URL. */ __( 'Do you really want to log out?' ), wp_logout_url( $redirect_to ) ); } else { $html = __( 'The link you followed has expired.' ); if ( wp_get_referer() ) { $wp_http_referer = remove_query_arg( 'updated', wp_get_referer() ); $wp_http_referer = wp_validate_redirect( sanitize_url( $wp_http_referer ) ); $html .= '
'; $html .= sprintf( '%s', esc_url( $wp_http_referer ), __( 'Please try again.' ) ); } } wp_die( $html, $title, $response_code ); } /** * Kills WordPress execution and displays HTML page with an error message. * * This function complements the `die()` PHP function. The difference is that * HTML will be displayed to the user. It is recommended to use this function * only when the execution should not continue any further. It is not recommended * to call this function very often, and try to handle as many errors as possible * silently or more gracefully. * * As a shorthand, the desired HTTP response code may be passed as an integer to * the `$title` parameter (the default title would apply) or the `$args` parameter. * * @since 2.0.4 * @since 4.1.0 The `$title` and `$args` parameters were changed to optionally accept * an integer to be used as the response code. * @since 5.1.0 The `$link_url`, `$link_text`, and `$exit` arguments were added. * @since 5.3.0 The `$charset` argument was added. * @since 5.5.0 The `$text_direction` argument has a priority over get_language_attributes() * in the default handler. * * @global WP_Query $wp_query WordPress Query object. * * @param string|WP_Error $message Optional. Error message. If this is a WP_Error object, * and not an Ajax or XML-RPC request, the error's messages are used. * Default empty string. * @param string|int $title Optional. Error title. If `$message` is a `WP_Error` object, * error data with the key 'title' may be used to specify the title. * If `$title` is an integer, then it is treated as the response code. * Default empty string. * @param string|array|int $args { * Optional. Arguments to control behavior. If `$args` is an integer, then it is treated * as the response code. Default empty array. * * @type int $response The HTTP response code. Default 200 for Ajax requests, 500 otherwise. * @type string $link_url A URL to include a link to. Only works in combination with $link_text. * Default empty string. * @type string $link_text A label for the link to include. Only works in combination with $link_url. * Default empty string. * @type bool $back_link Whether to include a link to go back. Default false. * @type string $text_direction The text direction. This is only useful internally, when WordPress is still * loading and the site's locale is not set up yet. Accepts 'rtl' and 'ltr'. * Default is the value of is_rtl(). * @type string $charset Character set of the HTML output. Default 'utf-8'. * @type string $code Error code to use. Default is 'wp_die', or the main error code if $message * is a WP_Error. * @type bool $exit Whether to exit the process after completion. Default true. * } */ function wp_die( $message = '', $title = '', $args = array() ) { global $wp_query; if ( is_int( $args ) ) { $args = array( 'response' => $args ); } elseif ( is_int( $title ) ) { $args = array( 'response' => $title ); $title = ''; } if ( wp_doing_ajax() ) { /** * Filters the callback for killing WordPress execution for Ajax requests. * * @since 3.4.0 * * @param callable $callback Callback function name. */ $callback = apply_filters( 'wp_die_ajax_handler', '_ajax_wp_die_handler' ); } elseif ( wp_is_json_request() ) { /** * Filters the callback for killing WordPress execution for JSON requests. * * @since 5.1.0 * * @param callable $callback Callback function name. */ $callback = apply_filters( 'wp_die_json_handler', '_json_wp_die_handler' ); } elseif ( wp_is_serving_rest_request() && wp_is_jsonp_request() ) { /** * Filters the callback for killing WordPress execution for JSONP REST requests. * * @since 5.2.0 * * @param callable $callback Callback function name. */ $callback = apply_filters( 'wp_die_jsonp_handler', '_jsonp_wp_die_handler' ); } elseif ( defined( 'XMLRPC_REQUEST' ) && XMLRPC_REQUEST ) { /** * Filters the callback for killing WordPress execution for XML-RPC requests. * * @since 3.4.0 * * @param callable $callback Callback function name. */ $callback = apply_filters( 'wp_die_xmlrpc_handler', '_xmlrpc_wp_die_handler' ); } elseif ( wp_is_xml_request() || isset( $wp_query ) && ( function_exists( 'is_feed' ) && is_feed() || function_exists( 'is_comment_feed' ) && is_comment_feed() || function_exists( 'is_trackback' ) && is_trackback() ) ) { /** * Filters the callback for killing WordPress execution for XML requests. * * @since 5.2.0 * * @param callable $callback Callback function name. */ $callback = apply_filters( 'wp_die_xml_handler', '_xml_wp_die_handler' ); } else { /** * Filters the callback for killing WordPress execution for all non-Ajax, non-JSON, non-XML requests. * * @since 3.0.0 * * @param callable $callback Callback function name. */ $callback = apply_filters( 'wp_die_handler', '_default_wp_die_handler' ); } call_user_func( $callback, $message, $title, $args ); } /** * Kills WordPress execution and displays HTML page with an error message. * * This is the default handler for wp_die(). If you want a custom one, * you can override this using the {@see 'wp_die_handler'} filter in wp_die(). * * @since 3.0.0 * @access private * * @param string|WP_Error $message Error message or WP_Error object. * @param string $title Optional. Error title. Default empty string. * @param string|array $args Optional. Arguments to control behavior. Default empty array. */ function _default_wp_die_handler( $message, $title = '', $args = array() ) { list( $message, $title, $parsed_args ) = _wp_die_process_input( $message, $title, $args ); if ( is_string( $message ) ) { if ( ! empty( $parsed_args['additional_errors'] ) ) { $message = array_merge( array( $message ), wp_list_pluck( $parsed_args['additional_errors'], 'message' ) ); $message = "
{$parsed_args['code']}
__construct()'
);
} else {
$message = sprintf(
/* translators: 1: PHP class name, 2: Version number, 3: __construct() method. */
__( 'The called constructor method for %1$s class is deprecated since version %2$s! Use %3$s instead.' ),
$class_name,
$version,
'__construct()'
);
}
} else {
if ( $parent_class ) {
$message = sprintf(
'The called constructor method for %1$s class in %2$s is deprecated since version %3$s! Use %4$s instead.',
$class_name,
$parent_class,
$version,
'__construct()'
);
} else {
$message = sprintf(
'The called constructor method for %1$s class is deprecated since version %2$s! Use %3$s instead.',
$class_name,
$version,
'__construct()'
);
}
}
wp_trigger_error( '', $message, E_USER_DEPRECATED );
}
}
/**
* Marks a class as deprecated and informs when it has been used.
*
* There is a {@see 'deprecated_class_run'} hook that will be called that can be used
* to get the backtrace up to what file and function called the deprecated class.
*
* The current behavior is to trigger a user error if `WP_DEBUG` is true.
*
* This function is to be used in the class constructor for every deprecated class.
* See {@see _deprecated_constructor()} for deprecating PHP4-style constructors.
*
* @since 6.4.0
*
* @param string $class_name The name of the class being instantiated.
* @param string $version The version of WordPress that deprecated the class.
* @param string $replacement Optional. The class or function that should have been called.
* Default empty string.
*/
function _deprecated_class( $class_name, $version, $replacement = '' ) {
/**
* Fires when a deprecated class is called.
*
* @since 6.4.0
*
* @param string $class_name The name of the class being instantiated.
* @param string $replacement The class or function that should have been called.
* @param string $version The version of WordPress that deprecated the class.
*/
do_action( 'deprecated_class_run', $class_name, $replacement, $version );
/**
* Filters whether to trigger an error for a deprecated class.
*
* @since 6.4.0
*
* @param bool $trigger Whether to trigger an error for a deprecated class. Default true.
*/
if ( WP_DEBUG && apply_filters( 'deprecated_class_trigger_error', true ) ) {
if ( function_exists( '__' ) ) {
if ( $replacement ) {
$message = sprintf(
/* translators: 1: PHP class name, 2: Version number, 3: Alternative class or function name. */
__( 'Class %1$s is deprecated since version %2$s! Use %3$s instead.' ),
$class_name,
$version,
$replacement
);
} else {
$message = sprintf(
/* translators: 1: PHP class name, 2: Version number. */
__( 'Class %1$s is deprecated since version %2$s with no alternative available.' ),
$class_name,
$version
);
}
} else {
if ( $replacement ) {
$message = sprintf(
'Class %1$s is deprecated since version %2$s! Use %3$s instead.',
$class_name,
$version,
$replacement
);
} else {
$message = sprintf(
'Class %1$s is deprecated since version %2$s with no alternative available.',
$class_name,
$version
);
}
}
wp_trigger_error( '', $message, E_USER_DEPRECATED );
}
}
/**
* Marks a file as deprecated and inform when it has been used.
*
* There is a {@see 'deprecated_file_included'} hook that will be called that can be used
* to get the backtrace up to what file and function included the deprecated file.
*
* The current behavior is to trigger a user error if `WP_DEBUG` is true.
*
* This function is to be used in every file that is deprecated.
*
* @since 2.5.0
* @since 5.4.0 This function is no longer marked as "private".
* @since 5.4.0 The error type is now classified as E_USER_DEPRECATED (used to default to E_USER_NOTICE).
*
* @param string $file The file that was included.
* @param string $version The version of WordPress that deprecated the file.
* @param string $replacement Optional. The file that should have been included based on ABSPATH.
* Default empty string.
* @param string $message Optional. A message regarding the change. Default empty string.
*/
function _deprecated_file( $file, $version, $replacement = '', $message = '' ) {
/**
* Fires when a deprecated file is called.
*
* @since 2.5.0
*
* @param string $file The file that was called.
* @param string $replacement The file that should have been included based on ABSPATH.
* @param string $version The version of WordPress that deprecated the file.
* @param string $message A message regarding the change.
*/
do_action( 'deprecated_file_included', $file, $replacement, $version, $message );
/**
* Filters whether to trigger an error for deprecated files.
*
* @since 2.5.0
*
* @param bool $trigger Whether to trigger the error for deprecated files. Default true.
*/
if ( WP_DEBUG && apply_filters( 'deprecated_file_trigger_error', true ) ) {
$message = empty( $message ) ? '' : ' ' . $message;
if ( function_exists( '__' ) ) {
if ( $replacement ) {
$message = sprintf(
/* translators: 1: PHP file name, 2: Version number, 3: Alternative file name. */
__( 'File %1$s is deprecated since version %2$s! Use %3$s instead.' ),
$file,
$version,
$replacement
) . $message;
} else {
$message = sprintf(
/* translators: 1: PHP file name, 2: Version number. */
__( 'File %1$s is deprecated since version %2$s with no alternative available.' ),
$file,
$version
) . $message;
}
} else {
if ( $replacement ) {
$message = sprintf(
'File %1$s is deprecated since version %2$s! Use %3$s instead.',
$file,
$version,
$replacement
);
} else {
$message = sprintf(
'File %1$s is deprecated since version %2$s with no alternative available.',
$file,
$version
) . $message;
}
}
wp_trigger_error( '', $message, E_USER_DEPRECATED );
}
}
/**
* Marks a function argument as deprecated and inform when it has been used.
*
* This function is to be used whenever a deprecated function argument is used.
* Before this function is called, the argument must be checked for whether it was
* used by comparing it to its default value or evaluating whether it is empty.
*
* For example:
*
* if ( ! empty( $deprecated ) ) {
* _deprecated_argument( __FUNCTION__, '3.0.0' );
* }
*
* There is a {@see 'deprecated_argument_run'} hook that will be called that can be used
* to get the backtrace up to what file and function used the deprecated argument.
*
* The current behavior is to trigger a user error if WP_DEBUG is true.
*
* @since 3.0.0
* @since 5.4.0 This function is no longer marked as "private".
* @since 5.4.0 The error type is now classified as E_USER_DEPRECATED (used to default to E_USER_NOTICE).
*
* @param string $function_name The function that was called.
* @param string $version The version of WordPress that deprecated the argument used.
* @param string $message Optional. A message regarding the change. Default empty string.
*/
function _deprecated_argument( $function_name, $version, $message = '' ) {
/**
* Fires when a deprecated argument is called.
*
* @since 3.0.0
*
* @param string $function_name The function that was called.
* @param string $message A message regarding the change.
* @param string $version The version of WordPress that deprecated the argument used.
*/
do_action( 'deprecated_argument_run', $function_name, $message, $version );
/**
* Filters whether to trigger an error for deprecated arguments.
*
* @since 3.0.0
*
* @param bool $trigger Whether to trigger the error for deprecated arguments. Default true.
*/
if ( WP_DEBUG && apply_filters( 'deprecated_argument_trigger_error', true ) ) {
if ( function_exists( '__' ) ) {
if ( $message ) {
$message = sprintf(
/* translators: 1: PHP function name, 2: Version number, 3: Optional message regarding the change. */
__( 'Function %1$s was called with an argument that is deprecated since version %2$s! %3$s' ),
$function_name,
$version,
$message
);
} else {
$message = sprintf(
/* translators: 1: PHP function name, 2: Version number. */
__( 'Function %1$s was called with an argument that is deprecated since version %2$s with no alternative available.' ),
$function_name,
$version
);
}
} else {
if ( $message ) {
$message = sprintf(
'Function %1$s was called with an argument that is deprecated since version %2$s! %3$s',
$function_name,
$version,
$message
);
} else {
$message = sprintf(
'Function %1$s was called with an argument that is deprecated since version %2$s with no alternative available.',
$function_name,
$version
);
}
}
wp_trigger_error( '', $message, E_USER_DEPRECATED );
}
}
/**
* Marks a deprecated action or filter hook as deprecated and throws a notice.
*
* Use the {@see 'deprecated_hook_run'} action to get the backtrace describing where
* the deprecated hook was called.
*
* Default behavior is to trigger a user error if `WP_DEBUG` is true.
*
* This function is called by the do_action_deprecated() and apply_filters_deprecated()
* functions, and so generally does not need to be called directly.
*
* @since 4.6.0
* @since 5.4.0 The error type is now classified as E_USER_DEPRECATED (used to default to E_USER_NOTICE).
* @access private
*
* @param string $hook The hook that was used.
* @param string $version The version of WordPress that deprecated the hook.
* @param string $replacement Optional. The hook that should have been used. Default empty string.
* @param string $message Optional. A message regarding the change. Default empty.
*/
function _deprecated_hook( $hook, $version, $replacement = '', $message = '' ) {
/**
* Fires when a deprecated hook is called.
*
* @since 4.6.0
*
* @param string $hook The hook that was called.
* @param string $replacement The hook that should be used as a replacement.
* @param string $version The version of WordPress that deprecated the argument used.
* @param string $message A message regarding the change.
*/
do_action( 'deprecated_hook_run', $hook, $replacement, $version, $message );
/**
* Filters whether to trigger deprecated hook errors.
*
* @since 4.6.0
*
* @param bool $trigger Whether to trigger deprecated hook errors. Requires
* `WP_DEBUG` to be defined true.
*/
if ( WP_DEBUG && apply_filters( 'deprecated_hook_trigger_error', true ) ) {
$message = empty( $message ) ? '' : ' ' . $message;
if ( $replacement ) {
$message = sprintf(
/* translators: 1: WordPress hook name, 2: Version number, 3: Alternative hook name. */
__( 'Hook %1$s is deprecated since version %2$s! Use %3$s instead.' ),
$hook,
$version,
$replacement
) . $message;
} else {
$message = sprintf(
/* translators: 1: WordPress hook name, 2: Version number. */
__( 'Hook %1$s is deprecated since version %2$s with no alternative available.' ),
$hook,
$version
) . $message;
}
wp_trigger_error( '', $message, E_USER_DEPRECATED );
}
}
/**
* Marks something as being incorrectly called.
*
* There is a {@see 'doing_it_wrong_run'} hook that will be called that can be used
* to get the backtrace up to what file and function called the deprecated function.
*
* The current behavior is to trigger a user error if `WP_DEBUG` is true.
*
* @since 3.1.0
* @since 5.4.0 This function is no longer marked as "private".
*
* @param string $function_name The function that was called.
* @param string $message A message explaining what has been done incorrectly.
* @param string $version The version of WordPress where the message was added.
*/
function _doing_it_wrong( $function_name, $message, $version ) {
/**
* Fires when the given function is being used incorrectly.
*
* @since 3.1.0
*
* @param string $function_name The function that was called.
* @param string $message A message explaining what has been done incorrectly.
* @param string $version The version of WordPress where the message was added.
*/
do_action( 'doing_it_wrong_run', $function_name, $message, $version );
/**
* Filters whether to trigger an error for _doing_it_wrong() calls.
*
* @since 3.1.0
* @since 5.1.0 Added the $function_name, $message and $version parameters.
*
* @param bool $trigger Whether to trigger the error for _doing_it_wrong() calls. Default true.
* @param string $function_name The function that was called.
* @param string $message A message explaining what has been done incorrectly.
* @param string $version The version of WordPress where the message was added.
*/
if ( WP_DEBUG && apply_filters( 'doing_it_wrong_trigger_error', true, $function_name, $message, $version ) ) {
if ( function_exists( '__' ) ) {
if ( $version ) {
/* translators: %s: Version number. */
$version = sprintf( __( '(This message was added in version %s.)' ), $version );
}
$message .= ' ' . sprintf(
/* translators: %s: Documentation URL. */
__( 'Please see Debugging in WordPress for more information.' ),
__( 'https://developer.wordpress.org/advanced-administration/debug/debug-wordpress/' )
);
$message = sprintf(
/* translators: Developer debugging message. 1: PHP function name, 2: Explanatory message, 3: WordPress version number. */
__( 'Function %1$s was called incorrectly. %2$s %3$s' ),
$function_name,
$message,
$version
);
} else {
if ( $version ) {
$version = sprintf( '(This message was added in version %s.)', $version );
}
$message .= sprintf(
' Please see Debugging in WordPress for more information.',
'https://developer.wordpress.org/advanced-administration/debug/debug-wordpress/'
);
$message = sprintf(
'Function %1$s was called incorrectly. %2$s %3$s',
$function_name,
$message,
$version
);
}
wp_trigger_error( '', $message );
}
}
/**
* Generates a user-level error/warning/notice/deprecation message.
*
* Generates the message when `WP_DEBUG` is true.
*
* @since 6.4.0
*
* @param string $function_name The function that triggered the error.
* @param string $message The message explaining the error.
* The message can contain allowed HTML 'a' (with href), 'code',
* 'br', 'em', and 'strong' tags and http or https protocols.
* If it contains other HTML tags or protocols, the message should be escaped
* before passing to this function to avoid being stripped {@see wp_kses()}.
* @param int $error_level Optional. The designated error type for this error.
* Only works with E_USER family of constants. Default E_USER_NOTICE.
*/
function wp_trigger_error( $function_name, $message, $error_level = E_USER_NOTICE ) {
// Bail out if WP_DEBUG is not turned on.
if ( ! WP_DEBUG ) {
return;
}
/**
* Fires when the given function triggers a user-level error/warning/notice/deprecation message.
*
* Can be used for debug backtracking.
*
* @since 6.4.0
*
* @param string $function_name The function that was called.
* @param string $message A message explaining what has been done incorrectly.
* @param int $error_level The designated error type for this error.
*/
do_action( 'wp_trigger_error_run', $function_name, $message, $error_level );
if ( ! empty( $function_name ) ) {
$message = sprintf( '%s(): %s', $function_name, $message );
}
$message = wp_kses(
$message,
array(
'a' => array( 'href' => true ),
'br' => array(),
'code' => array(),
'em' => array(),
'strong' => array(),
),
array( 'http', 'https' )
);
if ( E_USER_ERROR === $error_level ) {
throw new WP_Exception( $message );
}
trigger_error( $message, $error_level );
}
/**
* Determines whether the server is running an earlier than 1.5.0 version of lighttpd.
*
* @since 2.5.0
*
* @return bool Whether the server is running lighttpd < 1.5.0.
*/
function is_lighttpd_before_150() {
$server_parts = explode( '/', isset( $_SERVER['SERVER_SOFTWARE'] ) ? $_SERVER['SERVER_SOFTWARE'] : '' );
$server_parts[1] = isset( $server_parts[1] ) ? $server_parts[1] : '';
return ( 'lighttpd' === $server_parts[0] && -1 === version_compare( $server_parts[1], '1.5.0' ) );
}
/**
* Determines whether the specified module exist in the Apache config.
*
* @since 2.5.0
*
* @global bool $is_apache
*
* @param string $mod The module, e.g. mod_rewrite.
* @param bool $default_value Optional. The default return value if the module is not found. Default false.
* @return bool Whether the specified module is loaded.
*/
function apache_mod_loaded( $mod, $default_value = false ) {
global $is_apache;
if ( ! $is_apache ) {
return false;
}
$loaded_mods = array();
if ( function_exists( 'apache_get_modules' ) ) {
$loaded_mods = apache_get_modules();
if ( in_array( $mod, $loaded_mods, true ) ) {
return true;
}
}
if ( empty( $loaded_mods )
&& function_exists( 'phpinfo' )
&& ! str_contains( ini_get( 'disable_functions' ), 'phpinfo' )
) {
ob_start();
phpinfo( INFO_MODULES );
$phpinfo = ob_get_clean();
if ( str_contains( $phpinfo, $mod ) ) {
return true;
}
}
return $default_value;
}
/**
* Checks if IIS 7+ supports pretty permalinks.
*
* @since 2.8.0
*
* @global bool $is_iis7
*
* @return bool Whether IIS7 supports permalinks.
*/
function iis7_supports_permalinks() {
global $is_iis7;
$supports_permalinks = false;
if ( $is_iis7 ) {
/* First we check if the DOMDocument class exists. If it does not exist, then we cannot
* easily update the xml configuration file, hence we just bail out and tell user that
* pretty permalinks cannot be used.
*
* Next we check if the URL Rewrite Module 1.1 is loaded and enabled for the website. When
* URL Rewrite 1.1 is loaded it always sets a server variable called 'IIS_UrlRewriteModule'.
* Lastly we make sure that PHP is running via FastCGI. This is important because if it runs
* via ISAPI then pretty permalinks will not work.
*/
$supports_permalinks = class_exists( 'DOMDocument', false ) && isset( $_SERVER['IIS_UrlRewriteModule'] ) && ( 'cgi-fcgi' === PHP_SAPI );
}
/**
* Filters whether IIS 7+ supports pretty permalinks.
*
* @since 2.8.0
*
* @param bool $supports_permalinks Whether IIS7 supports permalinks. Default false.
*/
return apply_filters( 'iis7_supports_permalinks', $supports_permalinks );
}
/**
* Validates a file name and path against an allowed set of rules.
*
* A return value of `1` means the file path contains directory traversal.
*
* A return value of `2` means the file path contains a Windows drive path.
*
* A return value of `3` means the file is not in the allowed files list.
*
* @since 1.2.0
*
* @param string $file File path.
* @param string[] $allowed_files Optional. Array of allowed files. Default empty array.
* @return int 0 means nothing is wrong, greater than 0 means something was wrong.
*/
function validate_file( $file, $allowed_files = array() ) {
if ( ! is_scalar( $file ) || '' === $file ) {
return 0;
}
// Normalize path for Windows servers.
$file = wp_normalize_path( $file );
// Normalize path for $allowed_files as well so it's an apples to apples comparison.
$allowed_files = array_map( 'wp_normalize_path', $allowed_files );
// `../` on its own is not allowed:
if ( '../' === $file ) {
return 1;
}
// More than one occurrence of `../` is not allowed:
if ( preg_match_all( '#\.\./#', $file, $matches, PREG_SET_ORDER ) && ( count( $matches ) > 1 ) ) {
return 1;
}
// `../` which does not occur at the end of the path is not allowed:
if ( str_contains( $file, '../' ) && '../' !== mb_substr( $file, -3, 3 ) ) {
return 1;
}
// Files not in the allowed file list are not allowed:
if ( ! empty( $allowed_files ) && ! in_array( $file, $allowed_files, true ) ) {
return 3;
}
// Absolute Windows drive paths are not allowed:
if ( ':' === substr( $file, 1, 1 ) ) {
return 2;
}
return 0;
}
/**
* Determines whether to force SSL used for the Administration Screens.
*
* @since 2.6.0
*
* @param string|bool|null $force Optional. Whether to force SSL in admin screens. Default null.
* @return bool True if forced, false if not forced.
*/
function force_ssl_admin( $force = null ) {
static $forced = false;
if ( ! is_null( $force ) ) {
$old_forced = $forced;
$forced = (bool) $force;
return $old_forced;
}
return $forced;
}
/**
* Guesses the URL for the site.
*
* Will remove wp-admin links to retrieve only return URLs not in the wp-admin
* directory.
*
* @since 2.6.0
*
* @return string The guessed URL.
*/
function wp_guess_url() {
if ( defined( 'WP_SITEURL' ) && '' !== WP_SITEURL ) {
$url = WP_SITEURL;
} else {
$abspath_fix = str_replace( '\\', '/', ABSPATH );
$script_filename_dir = dirname( $_SERVER['SCRIPT_FILENAME'] );
// The request is for the admin.
if ( str_contains( $_SERVER['REQUEST_URI'], 'wp-admin' ) || str_contains( $_SERVER['REQUEST_URI'], 'wp-login.php' ) ) {
$path = preg_replace( '#/(wp-admin/?.*|wp-login\.php.*)#i', '', $_SERVER['REQUEST_URI'] );
// The request is for a file in ABSPATH.
} elseif ( $script_filename_dir . '/' === $abspath_fix ) {
// Strip off any file/query params in the path.
$path = preg_replace( '#/[^/]*$#i', '', $_SERVER['PHP_SELF'] );
} else {
if ( str_contains( $_SERVER['SCRIPT_FILENAME'], $abspath_fix ) ) {
// Request is hitting a file inside ABSPATH.
$directory = str_replace( ABSPATH, '', $script_filename_dir );
// Strip off the subdirectory, and any file/query params.
$path = preg_replace( '#/' . preg_quote( $directory, '#' ) . '/[^/]*$#i', '', $_SERVER['REQUEST_URI'] );
} elseif ( str_contains( $abspath_fix, $script_filename_dir ) ) {
// Request is hitting a file above ABSPATH.
$subdirectory = substr( $abspath_fix, strpos( $abspath_fix, $script_filename_dir ) + strlen( $script_filename_dir ) );
// Strip off any file/query params from the path, appending the subdirectory to the installation.
$path = preg_replace( '#/[^/]*$#i', '', $_SERVER['REQUEST_URI'] ) . $subdirectory;
} else {
$path = $_SERVER['REQUEST_URI'];
}
}
$schema = is_ssl() ? 'https://' : 'http://'; // set_url_scheme() is not defined yet.
$url = $schema . $_SERVER['HTTP_HOST'] . $path;
}
return rtrim( $url, '/' );
}
/**
* Temporarily suspends cache additions.
*
* Stops more data being added to the cache, but still allows cache retrieval.
* This is useful for actions, such as imports, when a lot of data would otherwise
* be almost uselessly added to the cache.
*
* Suspension lasts for a single page load at most. Remember to call this
* function again if you wish to re-enable cache adds earlier.
*
* @since 3.3.0
*
* @param bool $suspend Optional. Suspends additions if true, re-enables them if false.
* Defaults to not changing the current setting.
* @return bool The current suspend setting.
*/
function wp_suspend_cache_addition( $suspend = null ) {
static $_suspend = false;
if ( is_bool( $suspend ) ) {
$_suspend = $suspend;
}
return $_suspend;
}
/**
* Suspends cache invalidation.
*
* Turns cache invalidation on and off. Useful during imports where you don't want to do
* invalidations every time a post is inserted. Callers must be sure that what they are
* doing won't lead to an inconsistent cache when invalidation is suspended.
*
* @since 2.7.0
*
* @global bool $_wp_suspend_cache_invalidation
*
* @param bool $suspend Optional. Whether to suspend or enable cache invalidation. Default true.
* @return bool The current suspend setting.
*/
function wp_suspend_cache_invalidation( $suspend = true ) {
global $_wp_suspend_cache_invalidation;
$current_suspend = $_wp_suspend_cache_invalidation;
$_wp_suspend_cache_invalidation = $suspend;
return $current_suspend;
}
/**
* Determines whether a site is the main site of the current network.
*
* @since 3.0.0
* @since 4.9.0 The `$network_id` parameter was added.
*
* @param int $site_id Optional. Site ID to test. Defaults to current site.
* @param int $network_id Optional. Network ID of the network to check for.
* Defaults to current network.
* @return bool True if $site_id is the main site of the network, or if not
* running Multisite.
*/
function is_main_site( $site_id = null, $network_id = null ) {
if ( ! is_multisite() ) {
return true;
}
if ( ! $site_id ) {
$site_id = get_current_blog_id();
}
$site_id = (int) $site_id;
return get_main_site_id( $network_id ) === $site_id;
}
/**
* Gets the main site ID.
*
* @since 4.9.0
*
* @param int $network_id Optional. The ID of the network for which to get the main site.
* Defaults to the current network.
* @return int The ID of the main site.
*/
function get_main_site_id( $network_id = null ) {
if ( ! is_multisite() ) {
return get_current_blog_id();
}
$network = get_network( $network_id );
if ( ! $network ) {
return 0;
}
return $network->site_id;
}
/**
* Determines whether a network is the main network of the Multisite installation.
*
* @since 3.7.0
*
* @param int $network_id Optional. Network ID to test. Defaults to current network.
* @return bool True if $network_id is the main network, or if not running Multisite.
*/
function is_main_network( $network_id = null ) {
if ( ! is_multisite() ) {
return true;
}
if ( null === $network_id ) {
$network_id = get_current_network_id();
}
$network_id = (int) $network_id;
return ( get_main_network_id() === $network_id );
}
/**
* Gets the main network ID.
*
* @since 4.3.0
*
* @return int The ID of the main network.
*/
function get_main_network_id() {
if ( ! is_multisite() ) {
return 1;
}
$current_network = get_network();
if ( defined( 'PRIMARY_NETWORK_ID' ) ) {
$main_network_id = PRIMARY_NETWORK_ID;
} elseif ( isset( $current_network->id ) && 1 === (int) $current_network->id ) {
// If the current network has an ID of 1, assume it is the main network.
$main_network_id = 1;
} else {
$_networks = get_networks(
array(
'fields' => 'ids',
'number' => 1,
)
);
$main_network_id = array_shift( $_networks );
}
/**
* Filters the main network ID.
*
* @since 4.3.0
*
* @param int $main_network_id The ID of the main network.
*/
return (int) apply_filters( 'get_main_network_id', $main_network_id );
}
/**
* Determines whether site meta is enabled.
*
* This function checks whether the 'blogmeta' database table exists. The result is saved as
* a setting for the main network, making it essentially a global setting. Subsequent requests
* will refer to this setting instead of running the query.
*
* @since 5.1.0
*
* @global wpdb $wpdb WordPress database abstraction object.
*
* @return bool True if site meta is supported, false otherwise.
*/
function is_site_meta_supported() {
global $wpdb;
if ( ! is_multisite() ) {
return false;
}
$network_id = get_main_network_id();
$supported = get_network_option( $network_id, 'site_meta_supported', false );
if ( false === $supported ) {
$supported = $wpdb->get_var( "SHOW TABLES LIKE '{$wpdb->blogmeta}'" ) ? 1 : 0;
update_network_option( $network_id, 'site_meta_supported', $supported );
}
return (bool) $supported;
}
/**
* Modifies gmt_offset for smart timezone handling.
*
* Overrides the gmt_offset option if we have a timezone_string available.
*
* @since 2.8.0
*
* @return float|false Timezone GMT offset, false otherwise.
*/
function wp_timezone_override_offset() {
$timezone_string = get_option( 'timezone_string' );
if ( ! $timezone_string ) {
return false;
}
$timezone_object = timezone_open( $timezone_string );
$datetime_object = date_create();
if ( false === $timezone_object || false === $datetime_object ) {
return false;
}
return round( timezone_offset_get( $timezone_object, $datetime_object ) / HOUR_IN_SECONDS, 2 );
}
/**
* Sort-helper for timezones.
*
* @since 2.9.0
* @access private
*
* @param array $a
* @param array $b
* @return int
*/
function _wp_timezone_choice_usort_callback( $a, $b ) {
// Don't use translated versions of Etc.
if ( 'Etc' === $a['continent'] && 'Etc' === $b['continent'] ) {
// Make the order of these more like the old dropdown.
if ( str_starts_with( $a['city'], 'GMT+' ) && str_starts_with( $b['city'], 'GMT+' ) ) {
return -1 * ( strnatcasecmp( $a['city'], $b['city'] ) );
}
if ( 'UTC' === $a['city'] ) {
if ( str_starts_with( $b['city'], 'GMT+' ) ) {
return 1;
}
return -1;
}
if ( 'UTC' === $b['city'] ) {
if ( str_starts_with( $a['city'], 'GMT+' ) ) {
return -1;
}
return 1;
}
return strnatcasecmp( $a['city'], $b['city'] );
}
if ( $a['t_continent'] === $b['t_continent'] ) {
if ( $a['t_city'] === $b['t_city'] ) {
return strnatcasecmp( $a['t_subcity'], $b['t_subcity'] );
}
return strnatcasecmp( $a['t_city'], $b['t_city'] );
} else {
// Force Etc to the bottom of the list.
if ( 'Etc' === $a['continent'] ) {
return 1;
}
if ( 'Etc' === $b['continent'] ) {
return -1;
}
return strnatcasecmp( $a['t_continent'], $b['t_continent'] );
}
}
/**
* Gives a nicely-formatted list of timezone strings.
*
* @since 2.9.0
* @since 4.7.0 Added the `$locale` parameter.
*
* @param string $selected_zone Selected timezone.
* @param string $locale Optional. Locale to load the timezones in. Default current site locale.
* @return string
*/
function wp_timezone_choice( $selected_zone, $locale = null ) {
static $mo_loaded = false, $locale_loaded = null;
$continents = array( 'Africa', 'America', 'Antarctica', 'Arctic', 'Asia', 'Atlantic', 'Australia', 'Europe', 'Indian', 'Pacific' );
// Load translations for continents and cities.
if ( ! $mo_loaded || $locale !== $locale_loaded ) {
$locale_loaded = $locale ? $locale : get_locale();
$mofile = WP_LANG_DIR . '/continents-cities-' . $locale_loaded . '.mo';
unload_textdomain( 'continents-cities', true );
load_textdomain( 'continents-cities', $mofile, $locale_loaded );
$mo_loaded = true;
}
$tz_identifiers = timezone_identifiers_list();
$zonen = array();
foreach ( $tz_identifiers as $zone ) {
$zone = explode( '/', $zone );
if ( ! in_array( $zone[0], $continents, true ) ) {
continue;
}
// This determines what gets set and translated - we don't translate Etc/* strings here, they are done later.
$exists = array(
0 => ( isset( $zone[0] ) && $zone[0] ),
1 => ( isset( $zone[1] ) && $zone[1] ),
2 => ( isset( $zone[2] ) && $zone[2] ),
);
$exists[3] = ( $exists[0] && 'Etc' !== $zone[0] );
$exists[4] = ( $exists[1] && $exists[3] );
$exists[5] = ( $exists[2] && $exists[3] );
// phpcs:disable WordPress.WP.I18n.LowLevelTranslationFunction,WordPress.WP.I18n.NonSingularStringLiteralText
$zonen[] = array(
'continent' => ( $exists[0] ? $zone[0] : '' ),
'city' => ( $exists[1] ? $zone[1] : '' ),
'subcity' => ( $exists[2] ? $zone[2] : '' ),
't_continent' => ( $exists[3] ? translate( str_replace( '_', ' ', $zone[0] ), 'continents-cities' ) : '' ),
't_city' => ( $exists[4] ? translate( str_replace( '_', ' ', $zone[1] ), 'continents-cities' ) : '' ),
't_subcity' => ( $exists[5] ? translate( str_replace( '_', ' ', $zone[2] ), 'continents-cities' ) : '' ),
);
// phpcs:enable
}
usort( $zonen, '_wp_timezone_choice_usort_callback' );
$structure = array();
if ( empty( $selected_zone ) ) {
$structure[] = '';
}
// If this is a deprecated, but valid, timezone string, display it at the top of the list as-is.
if ( in_array( $selected_zone, $tz_identifiers, true ) === false
&& in_array( $selected_zone, timezone_identifiers_list( DateTimeZone::ALL_WITH_BC ), true )
) {
$structure[] = '';
}
foreach ( $zonen as $key => $zone ) {
// Build value in an array to join later.
$value = array( $zone['continent'] );
if ( empty( $zone['city'] ) ) {
// It's at the continent level (generally won't happen).
$display = $zone['t_continent'];
} else {
// It's inside a continent group.
// Continent optgroup.
if ( ! isset( $zonen[ $key - 1 ] ) || $zonen[ $key - 1 ]['continent'] !== $zone['continent'] ) {
$label = $zone['t_continent'];
$structure[] = '';
}
}
// Do UTC.
$structure[] = '';
// Do manual UTC offsets.
$structure[] = '';
return implode( "\n", $structure );
}
/**
* Strips close comment and close php tags from file headers used by WP.
*
* @since 2.8.0
* @access private
*
* @see https://core.trac.wordpress.org/ticket/8497
*
* @param string $str Header comment to clean up.
* @return string
*/
function _cleanup_header_comment( $str ) {
return trim( preg_replace( '/\s*(?:\*\/|\?>).*/', '', $str ) );
}
/**
* Permanently deletes comments or posts of any type that have held a status
* of 'trash' for the number of days defined in EMPTY_TRASH_DAYS.
*
* The default value of `EMPTY_TRASH_DAYS` is 30 (days).
*
* @since 2.9.0
*
* @global wpdb $wpdb WordPress database abstraction object.
*/
function wp_scheduled_delete() {
global $wpdb;
$delete_timestamp = time() - ( DAY_IN_SECONDS * EMPTY_TRASH_DAYS );
$posts_to_delete = $wpdb->get_results( $wpdb->prepare( "SELECT post_id FROM $wpdb->postmeta WHERE meta_key = '_wp_trash_meta_time' AND meta_value < %d", $delete_timestamp ), ARRAY_A );
foreach ( (array) $posts_to_delete as $post ) {
$post_id = (int) $post['post_id'];
if ( ! $post_id ) {
continue;
}
$del_post = get_post( $post_id );
if ( ! $del_post || 'trash' !== $del_post->post_status ) {
delete_post_meta( $post_id, '_wp_trash_meta_status' );
delete_post_meta( $post_id, '_wp_trash_meta_time' );
} else {
wp_delete_post( $post_id );
}
}
$comments_to_delete = $wpdb->get_results( $wpdb->prepare( "SELECT comment_id FROM $wpdb->commentmeta WHERE meta_key = '_wp_trash_meta_time' AND meta_value < %d", $delete_timestamp ), ARRAY_A );
foreach ( (array) $comments_to_delete as $comment ) {
$comment_id = (int) $comment['comment_id'];
if ( ! $comment_id ) {
continue;
}
$del_comment = get_comment( $comment_id );
if ( ! $del_comment || 'trash' !== $del_comment->comment_approved ) {
delete_comment_meta( $comment_id, '_wp_trash_meta_time' );
delete_comment_meta( $comment_id, '_wp_trash_meta_status' );
} else {
wp_delete_comment( $del_comment );
}
}
}
/**
* Retrieves metadata from a file.
*
* Searches for metadata in the first 8 KB of a file, such as a plugin or theme.
* Each piece of metadata must be on its own line. Fields can not span multiple
* lines, the value will get cut at the end of the first line.
*
* If the file data is not within that first 8 KB, then the author should correct
* their plugin file and move the data headers to the top.
*
* @link https://codex.wordpress.org/File_Header
*
* @since 2.9.0
*
* @param string $file Absolute path to the file.
* @param array $default_headers List of headers, in the format `array( 'HeaderKey' => 'Header Name' )`.
* @param string $context Optional. If specified adds filter hook {@see 'extra_$context_headers'}.
* Default empty string.
* @return string[] Array of file header values keyed by header name.
*/
function get_file_data( $file, $default_headers, $context = '' ) {
// Pull only the first 8 KB of the file in.
$file_data = file_get_contents( $file, false, null, 0, 8 * KB_IN_BYTES );
if ( false === $file_data ) {
$file_data = '';
}
// Make sure we catch CR-only line endings.
$file_data = str_replace( "\r", "\n", $file_data );
/**
* Filters extra file headers by context.
*
* The dynamic portion of the hook name, `$context`, refers to
* the context where extra headers might be loaded.
*
* @since 2.9.0
*
* @param array $extra_context_headers Empty array by default.
*/
$extra_headers = $context ? apply_filters( "extra_{$context}_headers", array() ) : array();
if ( $extra_headers ) {
$extra_headers = array_combine( $extra_headers, $extra_headers ); // Keys equal values.
$all_headers = array_merge( $extra_headers, (array) $default_headers );
} else {
$all_headers = $default_headers;
}
foreach ( $all_headers as $field => $regex ) {
if ( preg_match( '/^(?:[ \t]*<\?php)?[ \t\/*#@]*' . preg_quote( $regex, '/' ) . ':(.*)$/mi', $file_data, $match ) && $match[1] ) {
$all_headers[ $field ] = _cleanup_header_comment( $match[1] );
} else {
$all_headers[ $field ] = '';
}
}
return $all_headers;
}
/**
* Returns true.
*
* Useful for returning true to filters easily.
*
* @since 3.0.0
*
* @see __return_false()
*
* @return true True.
*/
function __return_true() { // phpcs:ignore WordPress.NamingConventions.ValidFunctionName.FunctionDoubleUnderscore,PHPCompatibility.FunctionNameRestrictions.ReservedFunctionNames.FunctionDoubleUnderscore
return true;
}
/**
* Returns false.
*
* Useful for returning false to filters easily.
*
* @since 3.0.0
*
* @see __return_true()
*
* @return false False.
*/
function __return_false() { // phpcs:ignore WordPress.NamingConventions.ValidFunctionName.FunctionDoubleUnderscore,PHPCompatibility.FunctionNameRestrictions.ReservedFunctionNames.FunctionDoubleUnderscore
return false;
}
/**
* Returns 0.
*
* Useful for returning 0 to filters easily.
*
* @since 3.0.0
*
* @return int 0.
*/
function __return_zero() { // phpcs:ignore WordPress.NamingConventions.ValidFunctionName.FunctionDoubleUnderscore,PHPCompatibility.FunctionNameRestrictions.ReservedFunctionNames.FunctionDoubleUnderscore
return 0;
}
/**
* Returns an empty array.
*
* Useful for returning an empty array to filters easily.
*
* @since 3.0.0
*
* @return array Empty array.
*/
function __return_empty_array() { // phpcs:ignore WordPress.NamingConventions.ValidFunctionName.FunctionDoubleUnderscore,PHPCompatibility.FunctionNameRestrictions.ReservedFunctionNames.FunctionDoubleUnderscore
return array();
}
/**
* Returns null.
*
* Useful for returning null to filters easily.
*
* @since 3.4.0
*
* @return null Null value.
*/
function __return_null() { // phpcs:ignore WordPress.NamingConventions.ValidFunctionName.FunctionDoubleUnderscore,PHPCompatibility.FunctionNameRestrictions.ReservedFunctionNames.FunctionDoubleUnderscore
return null;
}
/**
* Returns an empty string.
*
* Useful for returning an empty string to filters easily.
*
* @since 3.7.0
*
* @see __return_null()
*
* @return string Empty string.
*/
function __return_empty_string() { // phpcs:ignore WordPress.NamingConventions.ValidFunctionName.FunctionDoubleUnderscore,PHPCompatibility.FunctionNameRestrictions.ReservedFunctionNames.FunctionDoubleUnderscore
return '';
}
/**
* Sends a HTTP header to disable content type sniffing in browsers which support it.
*
* @since 3.0.0
*
* @see https://blogs.msdn.com/ie/archive/2008/07/02/ie8-security-part-v-comprehensive-protection.aspx
* @see https://src.chromium.org/viewvc/chrome?view=rev&revision=6985
*/
function send_nosniff_header() {
header( 'X-Content-Type-Options: nosniff' );
}
/**
* Returns a MySQL expression for selecting the week number based on the start_of_week option.
*
* @ignore
* @since 3.0.0
*
* @param string $column Database column.
* @return string SQL clause.
*/
function _wp_mysql_week( $column ) {
$start_of_week = (int) get_option( 'start_of_week' );
switch ( $start_of_week ) {
case 1:
return "WEEK( $column, 1 )";
case 2:
case 3:
case 4:
case 5:
case 6:
return "WEEK( DATE_SUB( $column, INTERVAL $start_of_week DAY ), 0 )";
case 0:
default:
return "WEEK( $column, 0 )";
}
}
/**
* Finds hierarchy loops using a callback function that maps object IDs to parent IDs.
*
* @since 3.1.0
* @access private
*
* @param callable $callback Function that accepts ( ID, $callback_args ) and outputs parent_ID.
* @param int $start The ID to start the loop check at.
* @param int $start_parent The parent_ID of $start to use instead of calling $callback( $start ).
* Use null to always use $callback.
* @param array $callback_args Optional. Additional arguments to send to $callback. Default empty array.
* @return array IDs of all members of loop.
*/
function wp_find_hierarchy_loop( $callback, $start, $start_parent, $callback_args = array() ) {
$override = is_null( $start_parent ) ? array() : array( $start => $start_parent );
$arbitrary_loop_member = wp_find_hierarchy_loop_tortoise_hare( $callback, $start, $override, $callback_args );
if ( ! $arbitrary_loop_member ) {
return array();
}
return wp_find_hierarchy_loop_tortoise_hare( $callback, $arbitrary_loop_member, $override, $callback_args, true );
}
/**
* Uses the "The Tortoise and the Hare" algorithm to detect loops.
*
* For every step of the algorithm, the hare takes two steps and the tortoise one.
* If the hare ever laps the tortoise, there must be a loop.
*
* @since 3.1.0
* @access private
*
* @param callable $callback Function that accepts ( ID, callback_arg, ... ) and outputs parent_ID.
* @param int $start The ID to start the loop check at.
* @param array $override Optional. An array of ( ID => parent_ID, ... ) to use instead of $callback.
* Default empty array.
* @param array $callback_args Optional. Additional arguments to send to $callback. Default empty array.
* @param bool $_return_loop Optional. Return loop members or just detect presence of loop? Only set
* to true if you already know the given $start is part of a loop (otherwise
* the returned array might include branches). Default false.
* @return mixed Scalar ID of some arbitrary member of the loop, or array of IDs of all members of loop if
* $_return_loop
*/
function wp_find_hierarchy_loop_tortoise_hare( $callback, $start, $override = array(), $callback_args = array(), $_return_loop = false ) {
$tortoise = $start;
$hare = $start;
$evanescent_hare = $start;
$return = array();
// Set evanescent_hare to one past hare. Increment hare two steps.
while (
$tortoise
&&
( $evanescent_hare = isset( $override[ $hare ] ) ? $override[ $hare ] : call_user_func_array( $callback, array_merge( array( $hare ), $callback_args ) ) )
&&
( $hare = isset( $override[ $evanescent_hare ] ) ? $override[ $evanescent_hare ] : call_user_func_array( $callback, array_merge( array( $evanescent_hare ), $callback_args ) ) )
) {
if ( $_return_loop ) {
$return[ $tortoise ] = true;
$return[ $evanescent_hare ] = true;
$return[ $hare ] = true;
}
// Tortoise got lapped - must be a loop.
if ( $tortoise === $evanescent_hare || $tortoise === $hare ) {
return $_return_loop ? $return : $tortoise;
}
// Increment tortoise by one step.
$tortoise = isset( $override[ $tortoise ] ) ? $override[ $tortoise ] : call_user_func_array( $callback, array_merge( array( $tortoise ), $callback_args ) );
}
return false;
}
/**
* Sends a HTTP header to limit rendering of pages to same origin iframes.
*
* @since 3.1.3
*
* @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/X-Frame-Options
* @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Security-Policy/frame-ancestors
*/
function send_frame_options_header() {
if ( ! headers_sent() ) {
header( 'X-Frame-Options: SAMEORIGIN' );
header( "Content-Security-Policy: frame-ancestors 'self';" );
}
}
/**
* Sends a referrer policy header so referrers are not sent externally from administration screens.
*
* @since 4.9.0
* @since 6.8.0 This function was moved from `wp-admin/includes/misc.php` to `wp-includes/functions.php`.
*/
function wp_admin_headers() {
$policy = 'strict-origin-when-cross-origin';
/**
* Filters the admin referrer policy header value.
*
* @since 4.9.0
* @since 4.9.5 The default value was changed to 'strict-origin-when-cross-origin'.
*
* @link https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Referrer-Policy
*
* @param string $policy The admin referrer policy header value. Default 'strict-origin-when-cross-origin'.
*/
$policy = apply_filters( 'admin_referrer_policy', $policy );
header( sprintf( 'Referrer-Policy: %s', $policy ) );
}
/**
* Retrieves a list of protocols to allow in HTML attributes.
*
* @since 3.3.0
* @since 4.3.0 Added 'webcal' to the protocols array.
* @since 4.7.0 Added 'urn' to the protocols array.
* @since 5.3.0 Added 'sms' to the protocols array.
* @since 5.6.0 Added 'irc6' and 'ircs' to the protocols array.
*
* @see wp_kses()
* @see esc_url()
*
* @return string[] Array of allowed protocols. Defaults to an array containing 'http', 'https',
* 'ftp', 'ftps', 'mailto', 'news', 'irc', 'irc6', 'ircs', 'gopher', 'nntp', 'feed',
* 'telnet', 'mms', 'rtsp', 'sms', 'svn', 'tel', 'fax', 'xmpp', 'webcal', and 'urn'.
* This covers all common link protocols, except for 'javascript' which should not
* be allowed for untrusted users.
*/
function wp_allowed_protocols() {
static $protocols = array();
if ( empty( $protocols ) ) {
$protocols = array( 'http', 'https', 'ftp', 'ftps', 'mailto', 'news', 'irc', 'irc6', 'ircs', 'gopher', 'nntp', 'feed', 'telnet', 'mms', 'rtsp', 'sms', 'svn', 'tel', 'fax', 'xmpp', 'webcal', 'urn' );
}
if ( ! did_action( 'wp_loaded' ) ) {
/**
* Filters the list of protocols allowed in HTML attributes.
*
* @since 3.0.0
*
* @param string[] $protocols Array of allowed protocols e.g. 'http', 'ftp', 'tel', and more.
*/
$protocols = array_unique( (array) apply_filters( 'kses_allowed_protocols', $protocols ) );
}
return $protocols;
}
/**
* Returns a comma-separated string or array of functions that have been called to get
* to the current point in code.
*
* @since 3.4.0
*
* @see https://core.trac.wordpress.org/ticket/19589
*
* @param string $ignore_class Optional. A class to ignore all function calls within - useful
* when you want to just give info about the callee. Default null.
* @param int $skip_frames Optional. A number of stack frames to skip - useful for unwinding
* back to the source of the issue. Default 0.
* @param bool $pretty Optional. Whether you want a comma separated string instead of
* the raw array returned. Default true.
* @return string|array Either a string containing a reversed comma separated trace or an array
* of individual calls.
*/
function wp_debug_backtrace_summary( $ignore_class = null, $skip_frames = 0, $pretty = true ) {
static $truncate_paths;
$trace = debug_backtrace( false );
$caller = array();
$check_class = ! is_null( $ignore_class );
++$skip_frames; // Skip this function.
if ( ! isset( $truncate_paths ) ) {
$truncate_paths = array(
wp_normalize_path( WP_CONTENT_DIR ),
wp_normalize_path( ABSPATH ),
);
}
foreach ( $trace as $call ) {
if ( $skip_frames > 0 ) {
--$skip_frames;
} elseif ( isset( $call['class'] ) ) {
if ( $check_class && $ignore_class === $call['class'] ) {
continue; // Filter out calls.
}
$caller[] = "{$call['class']}{$call['type']}{$call['function']}";
} else {
if ( in_array( $call['function'], array( 'do_action', 'apply_filters', 'do_action_ref_array', 'apply_filters_ref_array' ), true ) ) {
$caller[] = "{$call['function']}('{$call['args'][0]}')";
} elseif ( in_array( $call['function'], array( 'include', 'include_once', 'require', 'require_once' ), true ) ) {
$filename = isset( $call['args'][0] ) ? $call['args'][0] : '';
$caller[] = $call['function'] . "('" . str_replace( $truncate_paths, '', wp_normalize_path( $filename ) ) . "')";
} else {
$caller[] = $call['function'];
}
}
}
if ( $pretty ) {
return implode( ', ', array_reverse( $caller ) );
} else {
return $caller;
}
}
/**
* Retrieves IDs that are not already present in the cache.
*
* @since 3.4.0
* @since 6.1.0 This function is no longer marked as "private".
*
* @param int[] $object_ids Array of IDs.
* @param string $cache_group The cache group to check against.
* @return int[] Array of IDs not present in the cache.
*/
function _get_non_cached_ids( $object_ids, $cache_group ) {
$object_ids = array_filter( $object_ids, '_validate_cache_id' );
$object_ids = array_unique( array_map( 'intval', $object_ids ), SORT_NUMERIC );
if ( empty( $object_ids ) ) {
return array();
}
$non_cached_ids = array();
$cache_values = wp_cache_get_multiple( $object_ids, $cache_group );
foreach ( $cache_values as $id => $value ) {
if ( false === $value ) {
$non_cached_ids[] = (int) $id;
}
}
return $non_cached_ids;
}
/**
* Checks whether the given cache ID is either an integer or an integer-like string.
*
* Both `16` and `"16"` are considered valid, other numeric types and numeric strings
* (`16.3` and `"16.3"`) are considered invalid.
*
* @since 6.3.0
*
* @param mixed $object_id The cache ID to validate.
* @return bool Whether the given $object_id is a valid cache ID.
*/
function _validate_cache_id( $object_id ) {
/*
* filter_var() could be used here, but the `filter` PHP extension
* is considered optional and may not be available.
*/
if ( is_int( $object_id )
|| ( is_string( $object_id ) && (string) (int) $object_id === $object_id ) ) {
return true;
}
/* translators: %s: The type of the given object ID. */
$message = sprintf( __( 'Object ID must be an integer, %s given.' ), gettype( $object_id ) );
_doing_it_wrong( '_get_non_cached_ids', $message, '6.3.0' );
return false;
}
/**
* Tests if the current device has the capability to upload files.
*
* @since 3.4.0
* @access private
*
* @return bool Whether the device is able to upload files.
*/
function _device_can_upload() {
if ( ! wp_is_mobile() ) {
return true;
}
$ua = $_SERVER['HTTP_USER_AGENT'];
if ( str_contains( $ua, 'iPhone' )
|| str_contains( $ua, 'iPad' )
|| str_contains( $ua, 'iPod' ) ) {
return preg_match( '#OS ([\d_]+) like Mac OS X#', $ua, $version ) && version_compare( $version[1], '6', '>=' );
}
return true;
}
/**
* Tests if a given path is a stream URL
*
* @since 3.5.0
*
* @param string $path The resource path or URL.
* @return bool True if the path is a stream URL.
*/
function wp_is_stream( $path ) {
$scheme_separator = strpos( $path, '://' );
if ( false === $scheme_separator ) {
// $path isn't a stream.
return false;
}
$stream = substr( $path, 0, $scheme_separator );
return in_array( $stream, stream_get_wrappers(), true );
}
/**
* Tests if the supplied date is valid for the Gregorian calendar.
*
* @since 3.5.0
*
* @link https://www.php.net/manual/en/function.checkdate.php
*
* @param int $month Month number.
* @param int $day Day number.
* @param int $year Year number.
* @param string $source_date The date to filter.
* @return bool True if valid date, false if not valid date.
*/
function wp_checkdate( $month, $day, $year, $source_date ) {
$checkdate = false;
if ( is_numeric( $month ) && is_numeric( $day ) && is_numeric( $year ) ) {
$checkdate = checkdate( (int) $month, (int) $day, (int) $year );
}
/**
* Filters whether the given date is valid for the Gregorian calendar.
*
* @since 3.5.0
*
* @param bool $checkdate Whether the given date is valid.
* @param string $source_date Date to check.
*/
return apply_filters( 'wp_checkdate', $checkdate, $source_date );
}
/**
* Loads the auth check for monitoring whether the user is still logged in.
*
* Can be disabled with remove_action( 'admin_enqueue_scripts', 'wp_auth_check_load' );
*
* This is disabled for certain screens where a login screen could cause an
* inconvenient interruption. A filter called {@see 'wp_auth_check_load'} can be used
* for fine-grained control.
*
* @since 3.6.0
*/
function wp_auth_check_load() {
if ( ! is_admin() && ! is_user_logged_in() ) {
return;
}
if ( defined( 'IFRAME_REQUEST' ) ) {
return;
}
$screen = get_current_screen();
$hidden = array( 'update', 'update-network', 'update-core', 'update-core-network', 'upgrade', 'upgrade-network', 'network' );
$show = ! in_array( $screen->id, $hidden, true );
/**
* Filters whether to load the authentication check.
*
* Returning a falsey value from the filter will effectively short-circuit
* loading the authentication check.
*
* @since 3.6.0
*
* @param bool $show Whether to load the authentication check.
* @param WP_Screen $screen The current screen object.
*/
if ( apply_filters( 'wp_auth_check_load', $show, $screen ) ) {
wp_enqueue_style( 'wp-auth-check' );
wp_enqueue_script( 'wp-auth-check' );
add_action( 'admin_print_footer_scripts', 'wp_auth_check_html', 5 );
add_action( 'wp_print_footer_scripts', 'wp_auth_check_html', 5 );
}
}
/**
* Outputs the HTML that shows the wp-login dialog when the user is no longer logged in.
*
* @since 3.6.0
*/
function wp_auth_check_html() {
$login_url = wp_login_url();
$current_domain = ( is_ssl() ? 'https://' : 'http://' ) . $_SERVER['HTTP_HOST'];
$same_domain = str_starts_with( $login_url, $current_domain );
/**
* Filters whether the authentication check originated at the same domain.
*
* @since 3.6.0
*
* @param bool $same_domain Whether the authentication check originated at the same domain.
*/
$same_domain = apply_filters( 'wp_auth_check_same_domain', $same_domain );
$wrap_class = $same_domain ? 'hidden' : 'hidden fallback';
?>
[\s\S]*<\/%1$s>|\s*\/>)', tag_escape( $tag ) );
}
/**
* Indicates if a given slug for a character set represents the UTF-8
* text encoding. If not provided, examines the current blog's charset.
*
* A charset is considered to represent UTF-8 if it is a case-insensitive
* match of "UTF-8" with or without the hyphen.
*
* Example:
*
* true === is_utf8_charset( 'UTF-8' );
* true === is_utf8_charset( 'utf8' );
* false === is_utf8_charset( 'latin1' );
* false === is_utf8_charset( 'UTF 8' );
*
* // Only strings match.
* false === is_utf8_charset( [ 'charset' => 'utf-8' ] );
*
* // Without a given charset, it depends on the site option "blog_charset".
* $is_utf8 = is_utf8_charset();
*
* @since 6.6.0
* @since 6.6.1 A wrapper for _is_utf8_charset
*
* @see _is_utf8_charset
*
* @param string|null $blog_charset Optional. Slug representing a text character encoding, or "charset".
* E.g. "UTF-8", "Windows-1252", "ISO-8859-1", "SJIS".
* Default value is to infer from "blog_charset" option.
* @return bool Whether the slug represents the UTF-8 encoding.
*/
function is_utf8_charset( $blog_charset = null ) {
return _is_utf8_charset( $blog_charset ?? get_option( 'blog_charset' ) );
}
/**
* Retrieves a canonical form of the provided charset appropriate for passing to PHP
* functions such as htmlspecialchars() and charset HTML attributes.
*
* @since 3.6.0
* @access private
*
* @see https://core.trac.wordpress.org/ticket/23688
*
* @param string $charset A charset name, e.g. "UTF-8", "Windows-1252", "SJIS".
* @return string The canonical form of the charset.
*/
function _canonical_charset( $charset ) {
if ( is_utf8_charset( $charset ) ) {
return 'UTF-8';
}
/*
* Normalize the ISO-8859-1 family of languages.
*
* This is not required for htmlspecialchars(), as it properly recognizes all of
* the input character sets that here are transformed into "ISO-8859-1".
*
* @todo Should this entire check be removed since it's not required for the stated purpose?
* @todo Should WordPress transform other potential charset equivalents, such as "latin1"?
*/
if (
( 0 === strcasecmp( 'iso-8859-1', $charset ) ) ||
( 0 === strcasecmp( 'iso8859-1', $charset ) )
) {
return 'ISO-8859-1';
}
return $charset;
}
/**
* Sets the mbstring internal encoding to a binary safe encoding when func_overload
* is enabled.
*
* When mbstring.func_overload is in use for multi-byte encodings, the results from
* strlen() and similar functions respect the utf8 characters, causing binary data
* to return incorrect lengths.
*
* This function overrides the mbstring encoding to a binary-safe encoding, and
* resets it to the users expected encoding afterwards through the
* `reset_mbstring_encoding` function.
*
* It is safe to recursively call this function, however each
* `mbstring_binary_safe_encoding()` call must be followed up with an equal number
* of `reset_mbstring_encoding()` calls.
*
* @since 3.7.0
*
* @see reset_mbstring_encoding()
*
* @param bool $reset Optional. Whether to reset the encoding back to a previously-set encoding.
* Default false.
*/
function mbstring_binary_safe_encoding( $reset = false ) {
static $encodings = array();
static $overloaded = null;
if ( is_null( $overloaded ) ) {
if ( function_exists( 'mb_internal_encoding' )
&& ( (int) ini_get( 'mbstring.func_overload' ) & 2 ) // phpcs:ignore PHPCompatibility.IniDirectives.RemovedIniDirectives.mbstring_func_overloadDeprecated
) {
$overloaded = true;
} else {
$overloaded = false;
}
}
if ( false === $overloaded ) {
return;
}
if ( ! $reset ) {
$encoding = mb_internal_encoding();
array_push( $encodings, $encoding );
mb_internal_encoding( 'ISO-8859-1' );
}
if ( $reset && $encodings ) {
$encoding = array_pop( $encodings );
mb_internal_encoding( $encoding );
}
}
/**
* Resets the mbstring internal encoding to a users previously set encoding.
*
* @see mbstring_binary_safe_encoding()
*
* @since 3.7.0
*/
function reset_mbstring_encoding() {
mbstring_binary_safe_encoding( true );
}
/**
* Filters/validates a variable as a boolean.
*
* Alternative to `filter_var( $value, FILTER_VALIDATE_BOOLEAN )`.
*
* @since 4.0.0
*
* @param mixed $value Boolean value to validate.
* @return bool Whether the value is validated.
*/
function wp_validate_boolean( $value ) {
if ( is_bool( $value ) ) {
return $value;
}
if ( is_string( $value ) && 'false' === strtolower( $value ) ) {
return false;
}
return (bool) $value;
}
/**
* Deletes a file.
*
* @since 4.2.0
* @since 6.7.0 A return value was added.
*
* @param string $file The path to the file to delete.
* @return bool True on success, false on failure.
*/
function wp_delete_file( $file ) {
/**
* Filters the path of the file to delete.
*
* @since 2.1.0
*
* @param string $file Path to the file to delete.
*/
$delete = apply_filters( 'wp_delete_file', $file );
if ( ! empty( $delete ) ) {
return @unlink( $delete );
}
return false;
}
/**
* Deletes a file if its path is within the given directory.
*
* @since 4.9.7
*
* @param string $file Absolute path to the file to delete.
* @param string $directory Absolute path to a directory.
* @return bool True on success, false on failure.
*/
function wp_delete_file_from_directory( $file, $directory ) {
if ( wp_is_stream( $file ) ) {
$real_file = $file;
$real_directory = $directory;
} else {
$real_file = realpath( wp_normalize_path( $file ) );
$real_directory = realpath( wp_normalize_path( $directory ) );
}
if ( false !== $real_file ) {
$real_file = wp_normalize_path( $real_file );
}
if ( false !== $real_directory ) {
$real_directory = wp_normalize_path( $real_directory );
}
if ( false === $real_file || false === $real_directory || ! str_starts_with( $real_file, trailingslashit( $real_directory ) ) ) {
return false;
}
return wp_delete_file( $file );
}
/**
* Outputs a small JS snippet on preview tabs/windows to remove `window.name` when a user is navigating to another page.
*
* This prevents reusing the same tab for a preview when the user has navigated away.
*
* @since 4.3.0
*
* @global WP_Post $post Global post object.
*/
function wp_post_preview_js() {
global $post;
if ( ! is_preview() || empty( $post ) ) {
return;
}
// Has to match the window name used in post_submit_meta_box().
$name = 'wp-preview-' . (int) $post->ID;
ob_start();
?>
$wp_max_limit_int && $filtered_limit_int > $current_limit_int ) ) {
if ( false !== ini_set( 'memory_limit', $filtered_limit ) ) {
return $filtered_limit;
} else {
return false;
}
} elseif ( -1 === $wp_max_limit_int || $wp_max_limit_int > $current_limit_int ) {
if ( false !== ini_set( 'memory_limit', $wp_max_limit ) ) {
return $wp_max_limit;
} else {
return false;
}
}
return false;
}
/**
* Generates a random UUID (version 4).
*
* @since 4.7.0
*
* @return string UUID.
*/
function wp_generate_uuid4() {
return sprintf(
'%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
mt_rand( 0, 0xffff ),
mt_rand( 0, 0xffff ),
mt_rand( 0, 0xffff ),
mt_rand( 0, 0x0fff ) | 0x4000,
mt_rand( 0, 0x3fff ) | 0x8000,
mt_rand( 0, 0xffff ),
mt_rand( 0, 0xffff ),
mt_rand( 0, 0xffff )
);
}
/**
* Validates that a UUID is valid.
*
* @since 4.9.0
*
* @param mixed $uuid UUID to check.
* @param int $version Specify which version of UUID to check against. Default is none,
* to accept any UUID version. Otherwise, only version allowed is `4`.
* @return bool The string is a valid UUID or false on failure.
*/
function wp_is_uuid( $uuid, $version = null ) {
if ( ! is_string( $uuid ) ) {
return false;
}
if ( is_numeric( $version ) ) {
if ( 4 !== (int) $version ) {
_doing_it_wrong( __FUNCTION__, __( 'Only UUID V4 is supported at this time.' ), '4.9.0' );
return false;
}
$regex = '/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/';
} else {
$regex = '/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/';
}
return (bool) preg_match( $regex, $uuid );
}
/**
* Gets unique ID.
*
* This is a PHP implementation of Underscore's uniqueId method. A static variable
* contains an integer that is incremented with each call. This number is returned
* with the optional prefix. As such the returned value is not universally unique,
* but it is unique across the life of the PHP process.
*
* @since 5.0.3
*
* @param string $prefix Prefix for the returned ID.
* @return string Unique ID.
*/
function wp_unique_id( $prefix = '' ) {
static $id_counter = 0;
return $prefix . (string) ++$id_counter;
}
/**
* Generates an incremental ID that is independent per each different prefix.
*
* It is similar to `wp_unique_id`, but each prefix has its own internal ID
* counter to make each prefix independent from each other. The ID starts at 1
* and increments on each call. The returned value is not universally unique,
* but it is unique across the life of the PHP process and it's stable per
* prefix.
*
* @since 6.4.0
*
* @param string $prefix Optional. Prefix for the returned ID. Default empty string.
* @return string Incremental ID per prefix.
*/
function wp_unique_prefixed_id( $prefix = '' ) {
static $id_counters = array();
if ( ! is_string( $prefix ) ) {
wp_trigger_error(
__FUNCTION__,
sprintf( 'The prefix must be a string. "%s" data type given.', gettype( $prefix ) )
);
$prefix = '';
}
if ( ! isset( $id_counters[ $prefix ] ) ) {
$id_counters[ $prefix ] = 0;
}
$id = ++$id_counters[ $prefix ];
return $prefix . (string) $id;
}
/**
* Generates a unique ID based on the structure and values of a given array.
*
* This function serializes the array into a JSON string and generates a hash
* that serves as a unique identifier. Optionally, a prefix can be added to
* the generated ID for context or categorization.
*
* @since 6.8.0
*
* @param array $data The input array to generate an ID from.
* @param string $prefix Optional. A prefix to prepend to the generated ID. Default empty string.
* @return string The generated unique ID for the array.
*/
function wp_unique_id_from_values( array $data, string $prefix = '' ): string {
if ( empty( $data ) ) {
_doing_it_wrong(
__FUNCTION__,
sprintf(
/* translators: %s: The parameter name. */
__( 'The %s parameter must not be empty.' ),
'$data'
),
'6.8.0'
);
}
$serialized = wp_json_encode( $data );
$hash = substr( md5( $serialized ), 0, 8 );
return $prefix . $hash;
}
/**
* Gets last changed date for the specified cache group.
*
* @since 4.7.0
*
* @param string $group Where the cache contents are grouped.
* @return string UNIX timestamp with microseconds representing when the group was last changed.
*/
function wp_cache_get_last_changed( $group ) {
$last_changed = wp_cache_get( 'last_changed', $group );
if ( $last_changed ) {
return $last_changed;
}
return wp_cache_set_last_changed( $group );
}
/**
* Sets last changed date for the specified cache group to now.
*
* @since 6.3.0
*
* @param string $group Where the cache contents are grouped.
* @return string UNIX timestamp when the group was last changed.
*/
function wp_cache_set_last_changed( $group ) {
$previous_time = wp_cache_get( 'last_changed', $group );
$time = microtime();
wp_cache_set( 'last_changed', $time, $group );
/**
* Fires after a cache group `last_changed` time is updated.
* This may occur multiple times per page load and registered
* actions must be performant.
*
* @since 6.3.0
*
* @param string $group The cache group name.
* @param string $time The new last changed time (msec sec).
* @param string|false $previous_time The previous last changed time. False if not previously set.
*/
do_action( 'wp_cache_set_last_changed', $group, $time, $previous_time );
return $time;
}
/**
* Sends an email to the old site admin email address when the site admin email address changes.
*
* @since 4.9.0
*
* @param string $old_email The old site admin email address.
* @param string $new_email The new site admin email address.
* @param string $option_name The relevant database option name.
*/
function wp_site_admin_email_change_notification( $old_email, $new_email, $option_name ) {
$send = true;
// Don't send the notification for an empty email address or the default 'admin_email' value.
if ( empty( $old_email ) || 'you@example.com' === $old_email ) {
$send = false;
}
/**
* Filters whether to send the site admin email change notification email.
*
* @since 4.9.0
*
* @param bool $send Whether to send the email notification.
* @param string $old_email The old site admin email address.
* @param string $new_email The new site admin email address.
*/
$send = apply_filters( 'send_site_admin_email_change_email', $send, $old_email, $new_email );
if ( ! $send ) {
return;
}
/* translators: Do not translate OLD_EMAIL, NEW_EMAIL, SITENAME, SITEURL: those are placeholders. */
$email_change_text = __(
'Hi,
This notice confirms that the admin email address was changed on ###SITENAME###.
The new admin email address is ###NEW_EMAIL###.
This email has been sent to ###OLD_EMAIL###
Regards,
All at ###SITENAME###
###SITEURL###'
);
$email_change_email = array(
'to' => $old_email,
/* translators: Site admin email change notification email subject. %s: Site title. */
'subject' => __( '[%s] Admin Email Changed' ),
'message' => $email_change_text,
'headers' => '',
);
// Get site name.
$site_name = wp_specialchars_decode( get_option( 'blogname' ), ENT_QUOTES );
/**
* Filters the contents of the email notification sent when the site admin email address is changed.
*
* @since 4.9.0
*
* @param array $email_change_email {
* Used to build wp_mail().
*
* @type string $to The intended recipient.
* @type string $subject The subject of the email.
* @type string $message The content of the email.
* The following strings have a special meaning and will get replaced dynamically:
* - `###OLD_EMAIL###` The old site admin email address.
* - `###NEW_EMAIL###` The new site admin email address.
* - `###SITENAME###` The name of the site.
* - `###SITEURL###` The URL to the site.
* @type string $headers Headers.
* }
* @param string $old_email The old site admin email address.
* @param string $new_email The new site admin email address.
*/
$email_change_email = apply_filters( 'site_admin_email_change_email', $email_change_email, $old_email, $new_email );
$email_change_email['message'] = str_replace( '###OLD_EMAIL###', $old_email, $email_change_email['message'] );
$email_change_email['message'] = str_replace( '###NEW_EMAIL###', $new_email, $email_change_email['message'] );
$email_change_email['message'] = str_replace( '###SITENAME###', $site_name, $email_change_email['message'] );
$email_change_email['message'] = str_replace( '###SITEURL###', home_url(), $email_change_email['message'] );
wp_mail(
$email_change_email['to'],
sprintf(
$email_change_email['subject'],
$site_name
),
$email_change_email['message'],
$email_change_email['headers']
);
}
/**
* Returns an anonymized IPv4 or IPv6 address.
*
* @since 4.9.6 Abstracted from `WP_Community_Events::get_unsafe_client_ip()`.
*
* @param string $ip_addr The IPv4 or IPv6 address to be anonymized.
* @param bool $ipv6_fallback Optional. Whether to return the original IPv6 address if the needed functions
* to anonymize it are not present. Default false, return `::` (unspecified address).
* @return string The anonymized IP address.
*/
function wp_privacy_anonymize_ip( $ip_addr, $ipv6_fallback = false ) {
if ( empty( $ip_addr ) ) {
return '0.0.0.0';
}
// Detect what kind of IP address this is.
$ip_prefix = '';
$is_ipv6 = substr_count( $ip_addr, ':' ) > 1;
$is_ipv4 = ( 3 === substr_count( $ip_addr, '.' ) );
if ( $is_ipv6 && $is_ipv4 ) {
// IPv6 compatibility mode, temporarily strip the IPv6 part, and treat it like IPv4.
$ip_prefix = '::ffff:';
$ip_addr = preg_replace( '/^\[?[0-9a-f:]*:/i', '', $ip_addr );
$ip_addr = str_replace( ']', '', $ip_addr );
$is_ipv6 = false;
}
if ( $is_ipv6 ) {
// IPv6 addresses will always be enclosed in [] if there's a port.
$left_bracket = strpos( $ip_addr, '[' );
$right_bracket = strpos( $ip_addr, ']' );
$percent = strpos( $ip_addr, '%' );
$netmask = 'ffff:ffff:ffff:ffff:0000:0000:0000:0000';
// Strip the port (and [] from IPv6 addresses), if they exist.
if ( false !== $left_bracket && false !== $right_bracket ) {
$ip_addr = substr( $ip_addr, $left_bracket + 1, $right_bracket - $left_bracket - 1 );
} elseif ( false !== $left_bracket || false !== $right_bracket ) {
// The IP has one bracket, but not both, so it's malformed.
return '::';
}
// Strip the reachability scope.
if ( false !== $percent ) {
$ip_addr = substr( $ip_addr, 0, $percent );
}
// No invalid characters should be left.
if ( preg_match( '/[^0-9a-f:]/i', $ip_addr ) ) {
return '::';
}
// Partially anonymize the IP by reducing it to the corresponding network ID.
if ( function_exists( 'inet_pton' ) && function_exists( 'inet_ntop' ) ) {
$ip_addr = inet_ntop( inet_pton( $ip_addr ) & inet_pton( $netmask ) );
if ( false === $ip_addr ) {
return '::';
}
} elseif ( ! $ipv6_fallback ) {
return '::';
}
} elseif ( $is_ipv4 ) {
// Strip any port and partially anonymize the IP.
$last_octet_position = strrpos( $ip_addr, '.' );
$ip_addr = substr( $ip_addr, 0, $last_octet_position ) . '.0';
} else {
return '0.0.0.0';
}
// Restore the IPv6 prefix to compatibility mode addresses.
return $ip_prefix . $ip_addr;
}
/**
* Returns uniform "anonymous" data by type.
*
* @since 4.9.6
*
* @param string $type The type of data to be anonymized.
* @param string $data Optional. The data to be anonymized. Default empty string.
* @return string The anonymous data for the requested type.
*/
function wp_privacy_anonymize_data( $type, $data = '' ) {
switch ( $type ) {
case 'email':
$anonymous = 'deleted@site.invalid';
break;
case 'url':
$anonymous = 'https://site.invalid';
break;
case 'ip':
$anonymous = wp_privacy_anonymize_ip( $data );
break;
case 'date':
$anonymous = '0000-00-00 00:00:00';
break;
case 'text':
/* translators: Deleted text. */
$anonymous = __( '[deleted]' );
break;
case 'longtext':
/* translators: Deleted long text. */
$anonymous = __( 'This content was deleted by the author.' );
break;
default:
$anonymous = '';
break;
}
/**
* Filters the anonymous data for each type.
*
* @since 4.9.6
*
* @param string $anonymous Anonymized data.
* @param string $type Type of the data.
* @param string $data Original data.
*/
return apply_filters( 'wp_privacy_anonymize_data', $anonymous, $type, $data );
}
/**
* Returns the directory used to store personal data export files.
*
* @since 4.9.6
*
* @see wp_privacy_exports_url
*
* @return string Exports directory.
*/
function wp_privacy_exports_dir() {
$upload_dir = wp_upload_dir();
$exports_dir = trailingslashit( $upload_dir['basedir'] ) . 'wp-personal-data-exports/';
/**
* Filters the directory used to store personal data export files.
*
* @since 4.9.6
* @since 5.5.0 Exports now use relative paths, so changes to the directory
* via this filter should be reflected on the server.
*
* @param string $exports_dir Exports directory.
*/
return apply_filters( 'wp_privacy_exports_dir', $exports_dir );
}
/**
* Returns the URL of the directory used to store personal data export files.
*
* @since 4.9.6
*
* @see wp_privacy_exports_dir
*
* @return string Exports directory URL.
*/
function wp_privacy_exports_url() {
$upload_dir = wp_upload_dir();
$exports_url = trailingslashit( $upload_dir['baseurl'] ) . 'wp-personal-data-exports/';
/**
* Filters the URL of the directory used to store personal data export files.
*
* @since 4.9.6
* @since 5.5.0 Exports now use relative paths, so changes to the directory URL
* via this filter should be reflected on the server.
*
* @param string $exports_url Exports directory URL.
*/
return apply_filters( 'wp_privacy_exports_url', $exports_url );
}
/**
* Schedules a `WP_Cron` job to delete expired export files.
*
* @since 4.9.6
*/
function wp_schedule_delete_old_privacy_export_files() {
if ( wp_installing() ) {
return;
}
if ( ! wp_next_scheduled( 'wp_privacy_delete_old_export_files' ) ) {
wp_schedule_event( time(), 'hourly', 'wp_privacy_delete_old_export_files' );
}
}
/**
* Cleans up export files older than three days old.
*
* The export files are stored in `wp-content/uploads`, and are therefore publicly
* accessible. A CSPRN is appended to the filename to mitigate the risk of an
* unauthorized person downloading the file, but it is still possible. Deleting
* the file after the data subject has had a chance to delete it adds an additional
* layer of protection.
*
* @since 4.9.6
*/
function wp_privacy_delete_old_export_files() {
$exports_dir = wp_privacy_exports_dir();
if ( ! is_dir( $exports_dir ) ) {
return;
}
require_once ABSPATH . 'wp-admin/includes/file.php';
$export_files = list_files( $exports_dir, 100, array( 'index.php' ) );
/**
* Filters the lifetime, in seconds, of a personal data export file.
*
* By default, the lifetime is 3 days. Once the file reaches that age, it will automatically
* be deleted by a cron job.
*
* @since 4.9.6
*
* @param int $expiration The expiration age of the export, in seconds.
*/
$expiration = apply_filters( 'wp_privacy_export_expiration', 3 * DAY_IN_SECONDS );
foreach ( (array) $export_files as $export_file ) {
$file_age_in_seconds = time() - filemtime( $export_file );
if ( $expiration < $file_age_in_seconds ) {
unlink( $export_file );
}
}
}
/**
* Gets the URL to learn more about updating the PHP version the site is running on.
*
* This URL can be overridden by specifying an environment variable `WP_UPDATE_PHP_URL` or by using the
* {@see 'wp_update_php_url'} filter. Providing an empty string is not allowed and will result in the
* default URL being used. Furthermore the page the URL links to should preferably be localized in the
* site language.
*
* @since 5.1.0
*
* @return string URL to learn more about updating PHP.
*/
function wp_get_update_php_url() {
$default_url = wp_get_default_update_php_url();
$update_url = $default_url;
if ( false !== getenv( 'WP_UPDATE_PHP_URL' ) ) {
$update_url = getenv( 'WP_UPDATE_PHP_URL' );
}
/**
* Filters the URL to learn more about updating the PHP version the site is running on.
*
* Providing an empty string is not allowed and will result in the default URL being used. Furthermore
* the page the URL links to should preferably be localized in the site language.
*
* @since 5.1.0
*
* @param string $update_url URL to learn more about updating PHP.
*/
$update_url = apply_filters( 'wp_update_php_url', $update_url );
if ( empty( $update_url ) ) {
$update_url = $default_url;
}
return $update_url;
}
/**
* Gets the default URL to learn more about updating the PHP version the site is running on.
*
* Do not use this function to retrieve this URL. Instead, use {@see wp_get_update_php_url()} when relying on the URL.
* This function does not allow modifying the returned URL, and is only used to compare the actually used URL with the
* default one.
*
* @since 5.1.0
* @access private
*
* @return string Default URL to learn more about updating PHP.
*/
function wp_get_default_update_php_url() {
return _x( 'https://wordpress.org/support/update-php/', 'localized PHP upgrade information page' );
}
/**
* Prints the default annotation for the web host altering the "Update PHP" page URL.
*
* This function is to be used after {@see wp_get_update_php_url()} to display a consistent
* annotation if the web host has altered the default "Update PHP" page URL.
*
* @since 5.1.0
* @since 5.2.0 Added the `$before` and `$after` parameters.
* @since 6.4.0 Added the `$display` parameter.
*
* @param string $before Markup to output before the annotation. Default ``. * @param string $after Markup to output after the annotation. Default `
`. * @param bool $display Whether to echo or return the markup. Default `true` for echo. * * @return string|void */ function wp_update_php_annotation( $before = '', $after = '
', $display = true ) { $annotation = wp_get_update_php_annotation(); if ( $annotation ) { if ( $display ) { echo $before . $annotation . $after; } else { return $before . $annotation . $after; } } } /** * Returns the default annotation for the web hosting altering the "Update PHP" page URL. * * This function is to be used after {@see wp_get_update_php_url()} to return a consistent * annotation if the web host has altered the default "Update PHP" page URL. * * @since 5.2.0 * * @return string Update PHP page annotation. An empty string if no custom URLs are provided. */ function wp_get_update_php_annotation() { $update_url = wp_get_update_php_url(); $default_url = wp_get_default_update_php_url(); if ( $update_url === $default_url ) { return ''; } $annotation = sprintf( /* translators: %s: Default Update PHP page URL. */ __( 'This resource is provided by your web host, and is specific to your site. For more information, see the official WordPress documentation.' ), esc_url( $default_url ) ); return $annotation; } /** * Gets the URL for directly updating the PHP version the site is running on. * * A URL will only be returned if the `WP_DIRECT_UPDATE_PHP_URL` environment variable is specified or * by using the {@see 'wp_direct_php_update_url'} filter. This allows hosts to send users directly to * the page where they can update PHP to a newer version. * * @since 5.1.1 * * @return string URL for directly updating PHP or empty string. */ function wp_get_direct_php_update_url() { $direct_update_url = ''; if ( false !== getenv( 'WP_DIRECT_UPDATE_PHP_URL' ) ) { $direct_update_url = getenv( 'WP_DIRECT_UPDATE_PHP_URL' ); } /** * Filters the URL for directly updating the PHP version the site is running on from the host. * * @since 5.1.1 * * @param string $direct_update_url URL for directly updating PHP. */ $direct_update_url = apply_filters( 'wp_direct_php_update_url', $direct_update_url ); return $direct_update_url; } /** * Displays a button directly linking to a PHP update process. * * This provides hosts with a way for users to be sent directly to their PHP update process. * * The button is only displayed if a URL is returned by `wp_get_direct_php_update_url()`. * * @since 5.1.1 */ function wp_direct_php_update_button() { $direct_update_url = wp_get_direct_php_update_url(); if ( empty( $direct_update_url ) ) { return; } echo ''; } /** * Gets the URL to learn more about updating the site to use HTTPS. * * This URL can be overridden by specifying an environment variable `WP_UPDATE_HTTPS_URL` or by using the * {@see 'wp_update_https_url'} filter. Providing an empty string is not allowed and will result in the * default URL being used. Furthermore the page the URL links to should preferably be localized in the * site language. * * @since 5.7.0 * * @return string URL to learn more about updating to HTTPS. */ function wp_get_update_https_url() { $default_url = wp_get_default_update_https_url(); $update_url = $default_url; if ( false !== getenv( 'WP_UPDATE_HTTPS_URL' ) ) { $update_url = getenv( 'WP_UPDATE_HTTPS_URL' ); } /** * Filters the URL to learn more about updating the HTTPS version the site is running on. * * Providing an empty string is not allowed and will result in the default URL being used. Furthermore * the page the URL links to should preferably be localized in the site language. * * @since 5.7.0 * * @param string $update_url URL to learn more about updating HTTPS. */ $update_url = apply_filters( 'wp_update_https_url', $update_url ); if ( empty( $update_url ) ) { $update_url = $default_url; } return $update_url; } /** * Gets the default URL to learn more about updating the site to use HTTPS. * * Do not use this function to retrieve this URL. Instead, use {@see wp_get_update_https_url()} when relying on the URL. * This function does not allow modifying the returned URL, and is only used to compare the actually used URL with the * default one. * * @since 5.7.0 * @access private * * @return string Default URL to learn more about updating to HTTPS. */ function wp_get_default_update_https_url() { /* translators: Documentation explaining HTTPS and why it should be used. */ return __( 'https://developer.wordpress.org/advanced-administration/security/https/' ); } /** * Gets the URL for directly updating the site to use HTTPS. * * A URL will only be returned if the `WP_DIRECT_UPDATE_HTTPS_URL` environment variable is specified or * by using the {@see 'wp_direct_update_https_url'} filter. This allows hosts to send users directly to * the page where they can update their site to use HTTPS. * * @since 5.7.0 * * @return string URL for directly updating to HTTPS or empty string. */ function wp_get_direct_update_https_url() { $direct_update_url = ''; if ( false !== getenv( 'WP_DIRECT_UPDATE_HTTPS_URL' ) ) { $direct_update_url = getenv( 'WP_DIRECT_UPDATE_HTTPS_URL' ); } /** * Filters the URL for directly updating the PHP version the site is running on from the host. * * @since 5.7.0 * * @param string $direct_update_url URL for directly updating PHP. */ $direct_update_url = apply_filters( 'wp_direct_update_https_url', $direct_update_url ); return $direct_update_url; } /** * Gets the size of a directory. * * A helper function that is used primarily to check whether * a blog has exceeded its allowed upload space. * * @since MU (3.0.0) * @since 5.2.0 $max_execution_time parameter added. * * @param string $directory Full path of a directory. * @param int $max_execution_time Maximum time to run before giving up. In seconds. * The timeout is global and is measured from the moment WordPress started to load. * @return int|false|null Size in bytes if a valid directory. False if not. Null if timeout. */ function get_dirsize( $directory, $max_execution_time = null ) { /* * Exclude individual site directories from the total when checking the main site of a network, * as they are subdirectories and should not be counted. */ if ( is_multisite() && is_main_site() ) { $size = recurse_dirsize( $directory, $directory . '/sites', $max_execution_time ); } else { $size = recurse_dirsize( $directory, null, $max_execution_time ); } return $size; } /** * Gets the size of a directory recursively. * * Used by get_dirsize() to get a directory size when it contains other directories. * * @since MU (3.0.0) * @since 4.3.0 The `$exclude` parameter was added. * @since 5.2.0 The `$max_execution_time` parameter was added. * @since 5.6.0 The `$directory_cache` parameter was added. * * @param string $directory Full path of a directory. * @param string|string[] $exclude Optional. Full path of a subdirectory to exclude from the total, * or array of paths. Expected without trailing slash(es). * Default null. * @param int $max_execution_time Optional. Maximum time to run before giving up. In seconds. * The timeout is global and is measured from the moment * WordPress started to load. Defaults to the value of * `max_execution_time` PHP setting. * @param array $directory_cache Optional. Array of cached directory paths. * Defaults to the value of `dirsize_cache` transient. * @return int|false|null Size in bytes if a valid directory. False if not. Null if timeout. */ function recurse_dirsize( $directory, $exclude = null, $max_execution_time = null, &$directory_cache = null ) { $directory = untrailingslashit( $directory ); $save_cache = false; if ( ! isset( $directory_cache ) ) { $directory_cache = get_transient( 'dirsize_cache' ); $save_cache = true; } if ( isset( $directory_cache[ $directory ] ) && is_int( $directory_cache[ $directory ] ) ) { return $directory_cache[ $directory ]; } if ( ! file_exists( $directory ) || ! is_dir( $directory ) || ! is_readable( $directory ) ) { return false; } if ( ( is_string( $exclude ) && $directory === $exclude ) || ( is_array( $exclude ) && in_array( $directory, $exclude, true ) ) ) { return false; } if ( null === $max_execution_time ) { // Keep the previous behavior but attempt to prevent fatal errors from timeout if possible. if ( function_exists( 'ini_get' ) ) { $max_execution_time = ini_get( 'max_execution_time' ); } else { // Disable... $max_execution_time = 0; } // Leave 1 second "buffer" for other operations if $max_execution_time has reasonable value. if ( $max_execution_time > 10 ) { $max_execution_time -= 1; } } /** * Filters the amount of storage space used by one directory and all its children, in megabytes. * * Return the actual used space to short-circuit the recursive PHP file size calculation * and use something else, like a CDN API or native operating system tools for better performance. * * @since 5.6.0 * * @param int|false $space_used The amount of used space, in bytes. Default false. * @param string $directory Full path of a directory. * @param string|string[]|null $exclude Full path of a subdirectory to exclude from the total, * or array of paths. * @param int $max_execution_time Maximum time to run before giving up. In seconds. * @param array $directory_cache Array of cached directory paths. */ $size = apply_filters( 'pre_recurse_dirsize', false, $directory, $exclude, $max_execution_time, $directory_cache ); if ( false === $size ) { $size = 0; $handle = opendir( $directory ); if ( $handle ) { while ( ( $file = readdir( $handle ) ) !== false ) { $path = $directory . '/' . $file; if ( '.' !== $file && '..' !== $file ) { if ( is_file( $path ) ) { $size += filesize( $path ); } elseif ( is_dir( $path ) ) { $handlesize = recurse_dirsize( $path, $exclude, $max_execution_time, $directory_cache ); if ( $handlesize > 0 ) { $size += $handlesize; } } if ( $max_execution_time > 0 && ( microtime( true ) - WP_START_TIMESTAMP ) > $max_execution_time ) { // Time exceeded. Give up instead of risking a fatal timeout. $size = null; break; } } } closedir( $handle ); } } if ( ! is_array( $directory_cache ) ) { $directory_cache = array(); } $directory_cache[ $directory ] = $size; // Only write the transient on the top level call and not on recursive calls. if ( $save_cache ) { $expiration = ( wp_using_ext_object_cache() ) ? 0 : 10 * YEAR_IN_SECONDS; set_transient( 'dirsize_cache', $directory_cache, $expiration ); } return $size; } /** * Cleans directory size cache used by recurse_dirsize(). * * Removes the current directory and all parent directories from the `dirsize_cache` transient. * * @since 5.6.0 * @since 5.9.0 Added input validation with a notice for invalid input. * * @param string $path Full path of a directory or file. */ function clean_dirsize_cache( $path ) { if ( ! is_string( $path ) || empty( $path ) ) { wp_trigger_error( '', sprintf( /* translators: 1: Function name, 2: A variable type, like "boolean" or "integer". */ __( '%1$s only accepts a non-empty path string, received %2$s.' ), 'clean_dirsize_cache()',
'' . gettype( $path ) . ''
)
);
return;
}
$directory_cache = get_transient( 'dirsize_cache' );
if ( empty( $directory_cache ) ) {
return;
}
$expiration = ( wp_using_ext_object_cache() ) ? 0 : 10 * YEAR_IN_SECONDS;
if (
! str_contains( $path, '/' ) &&
! str_contains( $path, '\\' )
) {
unset( $directory_cache[ $path ] );
set_transient( 'dirsize_cache', $directory_cache, $expiration );
return;
}
$last_path = null;
$path = untrailingslashit( $path );
unset( $directory_cache[ $path ] );
while (
$last_path !== $path &&
DIRECTORY_SEPARATOR !== $path &&
'.' !== $path &&
'..' !== $path
) {
$last_path = $path;
$path = dirname( $path );
unset( $directory_cache[ $path ] );
}
set_transient( 'dirsize_cache', $directory_cache, $expiration );
}
/**
* Returns the current WordPress version.
*
* Returns an unmodified value of `$wp_version`. Some plugins modify the global
* in an attempt to improve security through obscurity. This practice can cause
* errors in WordPress, so the ability to get an unmodified version is needed.
*
* @since 6.7.0
*
* @return string The current WordPress version.
*/
function wp_get_wp_version() {
static $wp_version;
if ( ! isset( $wp_version ) ) {
require ABSPATH . WPINC . '/version.php';
}
return $wp_version;
}
/**
* Checks compatibility with the current WordPress version.
*
* @since 5.2.0
*
* @global string $_wp_tests_wp_version The WordPress version string. Used only in Core tests.
*
* @param string $required Minimum required WordPress version.
* @return bool True if required version is compatible or empty, false if not.
*/
function is_wp_version_compatible( $required ) {
if (
defined( 'WP_RUN_CORE_TESTS' )
&& WP_RUN_CORE_TESTS
&& isset( $GLOBALS['_wp_tests_wp_version'] )
) {
$wp_version = $GLOBALS['_wp_tests_wp_version'];
} else {
$wp_version = wp_get_wp_version();
}
// Strip off any -alpha, -RC, -beta, -src suffixes.
list( $version ) = explode( '-', $wp_version );
if ( is_string( $required ) ) {
$trimmed = trim( $required );
if ( substr_count( $trimmed, '.' ) > 1 && str_ends_with( $trimmed, '.0' ) ) {
$required = substr( $trimmed, 0, -2 );
}
}
return empty( $required ) || version_compare( $version, $required, '>=' );
}
/**
* Checks compatibility with the current PHP version.
*
* @since 5.2.0
*
* @param string $required Minimum required PHP version.
* @return bool True if required version is compatible or empty, false if not.
*/
function is_php_version_compatible( $required ) {
return empty( $required ) || version_compare( PHP_VERSION, $required, '>=' );
}
/**
* Checks if two numbers are nearly the same.
*
* This is similar to using `round()` but the precision is more fine-grained.
*
* @since 5.3.0
*
* @param int|float $expected The expected value.
* @param int|float $actual The actual number.
* @param int|float $precision Optional. The allowed variation. Default 1.
* @return bool Whether the numbers match within the specified precision.
*/
function wp_fuzzy_number_match( $expected, $actual, $precision = 1 ) {
return abs( (float) $expected - (float) $actual ) <= $precision;
}
/**
* Creates and returns the markup for an admin notice.
*
* @since 6.4.0
*
* @param string $message The message.
* @param array $args {
* Optional. An array of arguments for the admin notice. Default empty array.
*
* @type string $type Optional. The type of admin notice.
* For example, 'error', 'success', 'warning', 'info'.
* Default empty string.
* @type bool $dismissible Optional. Whether the admin notice is dismissible. Default false.
* @type string $id Optional. The value of the admin notice's ID attribute. Default empty string.
* @type string[] $additional_classes Optional. A string array of class names. Default empty array.
* @type string[] $attributes Optional. Additional attributes for the notice div. Default empty array.
* @type bool $paragraph_wrap Optional. Whether to wrap the message in paragraph tags. Default true.
* }
* @return string The markup for an admin notice.
*/
function wp_get_admin_notice( $message, $args = array() ) {
$defaults = array(
'type' => '',
'dismissible' => false,
'id' => '',
'additional_classes' => array(),
'attributes' => array(),
'paragraph_wrap' => true,
);
$args = wp_parse_args( $args, $defaults );
/**
* Filters the arguments for an admin notice.
*
* @since 6.4.0
*
* @param array $args The arguments for the admin notice.
* @param string $message The message for the admin notice.
*/
$args = apply_filters( 'wp_admin_notice_args', $args, $message );
$id = '';
$classes = 'notice';
$attributes = '';
if ( is_string( $args['id'] ) ) {
$trimmed_id = trim( $args['id'] );
if ( '' !== $trimmed_id ) {
$id = 'id="' . $trimmed_id . '" ';
}
}
if ( is_string( $args['type'] ) ) {
$type = trim( $args['type'] );
if ( str_contains( $type, ' ' ) ) {
_doing_it_wrong(
__FUNCTION__,
sprintf(
/* translators: %s: The "type" key. */
__( 'The %s key must be a string without spaces.' ),
'type'
),
'6.4.0'
);
}
if ( '' !== $type ) {
$classes .= ' notice-' . $type;
}
}
if ( true === $args['dismissible'] ) {
$classes .= ' is-dismissible';
}
if ( is_array( $args['additional_classes'] ) && ! empty( $args['additional_classes'] ) ) {
$classes .= ' ' . implode( ' ', $args['additional_classes'] );
}
if ( is_array( $args['attributes'] ) && ! empty( $args['attributes'] ) ) {
$attributes = '';
foreach ( $args['attributes'] as $attr => $val ) {
if ( is_bool( $val ) ) {
$attributes .= $val ? ' ' . $attr : '';
} elseif ( is_int( $attr ) ) {
$attributes .= ' ' . esc_attr( trim( $val ) );
} elseif ( $val ) {
$attributes .= ' ' . $attr . '="' . esc_attr( trim( $val ) ) . '"';
}
}
}
if ( false !== $args['paragraph_wrap'] ) {
$message = "$message
"; } $markup = sprintf( '| Inside TD?`. * * A BODY context node will produce the following tree: * * └─#text Inside TD? * * Notice that the ` | ` tags are completely ignored. * * Compare that with an SVG context node that produces the following tree: * * ├─svg:td * └─#text Inside TD? * * Here, a `td` node in the `svg` namespace is created, and its self-closing flag is respected. * This is a peculiarity of parsing HTML in foreign content like SVG. * * Finally, consider the tree produced with a TABLE context node: * * └─TBODY * └─TR * └─TD * └─#text Inside TD? * * These examples demonstrate how important the context node may be when processing an HTML * fragment. Special care must be taken when processing fragments that are expected to appear * in specific contexts. SVG and TABLE are good examples, but there are others. * * @see https://html.spec.whatwg.org/multipage/parsing.html#html-fragment-parsing-algorithm * * @since 6.8.0 * * @param string $html Input HTML fragment to process. * @return static|null The created processor if successful, otherwise null. */ private function create_fragment_at_current_node( string $html ) { if ( $this->get_token_type() !== '#tag' || $this->is_tag_closer() ) { _doing_it_wrong( __METHOD__, __( 'The context element must be a start tag.' ), '6.8.0' ); return null; } $tag_name = $this->current_element->token->node_name; $namespace = $this->current_element->token->namespace; if ( 'html' === $namespace && self::is_void( $tag_name ) ) { _doing_it_wrong( __METHOD__, sprintf( // translators: %s: A tag name like INPUT or BR. __( 'The context element cannot be a void element, found "%s".' ), $tag_name ), '6.8.0' ); return null; } /* * Prevent creating fragments at nodes that require a special tokenizer state. * This is unsupported by the HTML Processor. */ if ( 'html' === $namespace && in_array( $tag_name, array( 'IFRAME', 'NOEMBED', 'NOFRAMES', 'SCRIPT', 'STYLE', 'TEXTAREA', 'TITLE', 'XMP', 'PLAINTEXT' ), true ) ) { _doing_it_wrong( __METHOD__, sprintf( // translators: %s: A tag name like IFRAME or TEXTAREA. __( 'The context element "%s" is not supported.' ), $tag_name ), '6.8.0' ); return null; } $fragment_processor = new static( $html, self::CONSTRUCTOR_UNLOCK_CODE ); $fragment_processor->compat_mode = $this->compat_mode; // @todo Create "fake" bookmarks for non-existent but implied nodes. $fragment_processor->bookmarks['root-node'] = new WP_HTML_Span( 0, 0 ); $root_node = new WP_HTML_Token( 'root-node', 'HTML', false ); $fragment_processor->state->stack_of_open_elements->push( $root_node ); $fragment_processor->bookmarks['context-node'] = new WP_HTML_Span( 0, 0 ); $fragment_processor->context_node = clone $this->current_element->token; $fragment_processor->context_node->bookmark_name = 'context-node'; $fragment_processor->context_node->on_destroy = null; $fragment_processor->breadcrumbs = array( 'HTML', $fragment_processor->context_node->node_name ); if ( 'TEMPLATE' === $fragment_processor->context_node->node_name ) { $fragment_processor->state->stack_of_template_insertion_modes[] = WP_HTML_Processor_State::INSERTION_MODE_IN_TEMPLATE; } $fragment_processor->reset_insertion_mode_appropriately(); /* * > Set the parser's form element pointer to the nearest node to the context element that * > is a form element (going straight up the ancestor chain, and including the element * > itself, if it is a form element), if any. (If there is no such form element, the * > form element pointer keeps its initial value, null.) */ foreach ( $this->state->stack_of_open_elements->walk_up() as $element ) { if ( 'FORM' === $element->node_name && 'html' === $element->namespace ) { $fragment_processor->state->form_element = clone $element; $fragment_processor->state->form_element->bookmark_name = null; $fragment_processor->state->form_element->on_destroy = null; break; } } $fragment_processor->state->encoding_confidence = 'irrelevant'; /* * Update the parsing namespace near the end of the process. * This is important so that any push/pop from the stack of open * elements does not change the parsing namespace. */ $fragment_processor->change_parsing_namespace( $this->current_element->token->integration_node_type ? 'html' : $namespace ); return $fragment_processor; } /** * Stops the parser and terminates its execution when encountering unsupported markup. * * @throws WP_HTML_Unsupported_Exception Halts execution of the parser. * * @since 6.7.0 * * @param string $message Explains support is missing in order to parse the current node. */ private function bail( string $message ) { $here = $this->bookmarks[ $this->state->current_token->bookmark_name ]; $token = substr( $this->html, $here->start, $here->length ); $open_elements = array(); foreach ( $this->state->stack_of_open_elements->stack as $item ) { $open_elements[] = $item->node_name; } $active_formats = array(); foreach ( $this->state->active_formatting_elements->walk_down() as $item ) { $active_formats[] = $item->node_name; } $this->last_error = self::ERROR_UNSUPPORTED; $this->unsupported_exception = new WP_HTML_Unsupported_Exception( $message, $this->state->current_token->node_name, $here->start, $token, $open_elements, $active_formats ); throw $this->unsupported_exception; } /** * Returns the last error, if any. * * Various situations lead to parsing failure but this class will * return `false` in all those cases. To determine why something * failed it's possible to request the last error. This can be * helpful to know to distinguish whether a given tag couldn't * be found or if content in the document caused the processor * to give up and abort processing. * * Example * * $processor = WP_HTML_Processor::create_fragment( ' |