=> '4.17.19', 'wp-polyfill-fetch' => '3.6.17', 'wp-polyfill-formdata' => '4.0.10', 'wp-polyfill-node-contains' => '4.8.0', 'wp-polyfill-url' => '3.6.4', 'wp-polyfill-dom-rect' => '4.8.0', 'wp-polyfill-element-closest' => '3.0.2', 'wp-polyfill-object-fit' => '2.3.5', 'wp-polyfill-inert' => '3.1.2', 'wp-polyfill' => '3.15.0', ); foreach ( $vendor_scripts as $handle => $dependencies ) { if ( is_string( $dependencies ) ) { $handle = $dependencies; $dependencies = array(); } $path = "/wp-includes/js/dist/vendor/$handle$suffix.js"; $version = $vendor_scripts_versions[ $handle ]; $scripts->add( $handle, $path, $dependencies, $version, 1 ); } did_action( 'init' ) && $scripts->add_inline_script( 'lodash', 'window.lodash = _.noConflict();' ); did_action( 'init' ) && $scripts->add_inline_script( 'moment', sprintf( "moment.updateLocale( '%s', %s );", get_user_locale(), wp_json_encode( array( 'months' => array_values( $wp_locale->month ), 'monthsShort' => array_values( $wp_locale->month_abbrev ), 'weekdays' => array_values( $wp_locale->weekday ), 'weekdaysShort' => array_values( $wp_locale->weekday_abbrev ), 'week' => array( 'dow' => (int) get_option( 'start_of_week', 0 ), ), 'longDateFormat' => array( 'LT' => get_option( 'time_format', __( 'g:i a' ) ), 'LTS' => null, 'L' => null, 'LL' => get_option( 'date_format', __( 'F j, Y' ) ), 'LLL' => __( 'F j, Y g:i a' ), 'LLLL' => null, ), ) ) ), 'after' ); } /** * Returns contents of an inline script used in appending polyfill scripts for * browsers which fail the provided tests. The provided array is a mapping from * a condition to verify feature support to its polyfill script handle. * * @since 5.0.0 * * @param WP_Scripts $scripts WP_Scripts object. * @param string[] $tests Features to detect. * @return string Conditional polyfill inline script. */ function wp_get_script_polyfill( $scripts, $tests ) { $polyfill = ''; foreach ( $tests as $test => $handle ) { if ( ! array_key_exists( $handle, $scripts->registered ) ) { continue; } $src = $scripts->registered[ $handle ]->src; $ver = $scripts->registered[ $handle ]->ver; if ( ! preg_match( '|^(https?:)?//|', $src ) && ! ( $scripts->content_url && str_starts_with( $src, $scripts->content_url ) ) ) { $src = $scripts->base_url . $src; } if ( ! empty( $ver ) ) { $src = add_query_arg( 'ver', $ver, $src ); } /** This filter is documented in wp-includes/class-wp-scripts.php */ $src = esc_url( apply_filters( 'script_loader_src', $src, $handle ) ); if ( ! $src ) { continue; } $polyfill .= ( // Test presence of feature... '( ' . $test . ' ) || ' . /* * ...appending polyfill on any failures. Cautious viewers may balk * at the `document.write`. Its caveat of synchronous mid-stream * blocking write is exactly the behavior we need though. */ 'document.write( \'\n"; } $concat = str_split( $concat, 128 ); $concatenated = ''; foreach ( $concat as $key => $chunk ) { $concatenated .= "&load%5Bchunk_{$key}%5D={$chunk}"; } $src = $wp_scripts->base_url . "/wp-admin/load-scripts.php?c={$zip}" . $concatenated . '&ver=' . $wp_scripts->default_version; echo "\n"; } if ( ! empty( $wp_scripts->print_html ) ) { echo $wp_scripts->print_html; } } /** * Prints the script queue in the HTML head on the front end. * * Postpones the scripts that were queued for the footer. * wp_print_footer_scripts() is called in the footer to print these scripts. * * @since 2.8.0 * * @global WP_Scripts $wp_scripts * * @return array */ function wp_print_head_scripts() { global $wp_scripts; if ( ! did_action( 'wp_print_scripts' ) ) { /** This action is documented in wp-includes/functions.wp-scripts.php */ do_action( 'wp_print_scripts' ); } if ( ! ( $wp_scripts instanceof WP_Scripts ) ) { return array(); // No need to run if nothing is queued. } return print_head_scripts(); } /** * Private, for use in *_footer_scripts hooks * * @since 3.3.0 */ function _wp_footer_scripts() { print_late_styles(); print_footer_scripts(); } /** * Hooks to print the scripts and styles in the footer. * * @since 2.8.0 */ function wp_print_footer_scripts() { /** * Fires when footer scripts are printed. * * @since 2.8.0 */ do_action( 'wp_print_footer_scripts' ); } /** * Wrapper for do_action( 'wp_enqueue_scripts' ). * * Allows plugins to queue scripts for the front end using wp_enqueue_script(). * Runs first in wp_head() where all is_home(), is_page(), etc. functions are available. * * @since 2.8.0 */ function wp_enqueue_scripts() { /** * Fires when scripts and styles are enqueued. * * @since 2.8.0 */ do_action( 'wp_enqueue_scripts' ); } /** * Prints the styles queue in the HTML head on admin pages. * * @since 2.8.0 * * @global bool $concatenate_scripts * * @return array */ function print_admin_styles() { global $concatenate_scripts; $wp_styles = wp_styles(); script_concat_settings(); $wp_styles->do_concat = $concatenate_scripts; $wp_styles->do_items( false ); /** * Filters whether to print the admin styles. * * @since 2.8.0 * * @param bool $print Whether to print the admin styles. Default true. */ if ( apply_filters( 'print_admin_styles', true ) ) { _print_styles(); } $wp_styles->reset(); return $wp_styles->done; } /** * Prints the styles that were queued too late for the HTML head. * * @since 3.3.0 * * @global WP_Styles $wp_styles * @global bool $concatenate_scripts * * @return array|void */ function print_late_styles() { global $wp_styles, $concatenate_scripts; if ( ! ( $wp_styles instanceof WP_Styles ) ) { return; } script_concat_settings(); $wp_styles->do_concat = $concatenate_scripts; $wp_styles->do_footer_items(); /** * Filters whether to print the styles queued too late for the HTML head. * * @since 3.3.0 * * @param bool $print Whether to print the 'late' styles. Default true. */ if ( apply_filters( 'print_late_styles', true ) ) { _print_styles(); } $wp_styles->reset(); return $wp_styles->done; } /** * Prints styles (internal use only). * * @ignore * @since 3.3.0 * * @global bool $compress_css */ function _print_styles() { global $compress_css; $wp_styles = wp_styles(); $zip = $compress_css ? 1 : 0; if ( $zip && defined( 'ENFORCE_GZIP' ) && ENFORCE_GZIP ) { $zip = 'gzip'; } $concat = trim( $wp_styles->concat, ', ' ); $type_attr = current_theme_supports( 'html5', 'style' ) ? '' : ' type="text/css"'; if ( $concat ) { $dir = $wp_styles->text_direction; $ver = $wp_styles->default_version; $concat = str_split( $concat, 128 ); $concatenated = ''; foreach ( $concat as $key => $chunk ) { $concatenated .= "&load%5Bchunk_{$key}%5D={$chunk}"; } $href = $wp_styles->base_url . "/wp-admin/load-styles.php?c={$zip}&dir={$dir}" . $concatenated . '&ver=' . $ver; echo "\n"; if ( ! empty( $wp_styles->print_code ) ) { echo "\n"; echo $wp_styles->print_code; echo "\n\n"; } } if ( ! empty( $wp_styles->print_html ) ) { echo $wp_styles->print_html; } } /** * Determines the concatenation and compression settings for scripts and styles. * * @since 2.8.0 * * @global bool $concatenate_scripts * @global bool $compress_scripts * @global bool $compress_css */ function script_concat_settings() { global $concatenate_scripts, $compress_scripts, $compress_css; $compressed_output = ( ini_get( 'zlib.output_compression' ) || 'ob_gzhandler' === ini_get( 'output_handler' ) ); $can_compress_scripts = ! wp_installing() && get_site_option( 'can_compress_scripts' ); if ( ! isset( $concatenate_scripts ) ) { $concatenate_scripts = defined( 'CONCATENATE_SCRIPTS' ) ? CONCATENATE_SCRIPTS : true; if ( ( ! is_admin() && ! did_action( 'login_init' ) ) || ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) ) { $concatenate_scripts = false; } } if ( ! isset( $compress_scripts ) ) { $compress_scripts = defined( 'COMPRESS_SCRIPTS' ) ? COMPRESS_SCRIPTS : true; if ( $compress_scripts && ( ! $can_compress_scripts || $compressed_output ) ) { $compress_scripts = false; } } if ( ! isset( $compress_css ) ) { $compress_css = defined( 'COMPRESS_CSS' ) ? COMPRESS_CSS : true; if ( $compress_css && ( ! $can_compress_scripts || $compressed_output ) ) { $compress_css = false; } } } /** * Handles the enqueueing of block scripts and styles that are common to both * the editor and the front-end. * * @since 5.0.0 */ function wp_common_block_scripts_and_styles() { if ( is_admin() && ! wp_should_load_block_editor_scripts_and_styles() ) { return; } wp_enqueue_style( 'wp-block-library' ); if ( current_theme_supports( 'wp-block-styles' ) && ! wp_should_load_separate_core_block_assets() ) { wp_enqueue_style( 'wp-block-library-theme' ); } /** * Fires after enqueuing block assets for both editor and front-end. * * Call `add_action` on any hook before 'wp_enqueue_scripts'. * * In the function call you supply, simply use `wp_enqueue_script` and * `wp_enqueue_style` to add your functionality to the Gutenberg editor. * * @since 5.0.0 */ do_action( 'enqueue_block_assets' ); } /** * Applies a filter to the list of style nodes that comes from WP_Theme_JSON::get_style_nodes(). * * This particular filter removes all of the blocks from the array. * * We want WP_Theme_JSON to be ignorant of the implementation details of how the CSS is being used. * This filter allows us to modify the output of WP_Theme_JSON depending on whether or not we are * loading separate assets, without making the class aware of that detail. * * @since 6.1.0 * * @param array $nodes The nodes to filter. * @return array A filtered array of style nodes. */ function wp_filter_out_block_nodes( $nodes ) { return array_filter( $nodes, static function ( $node ) { return ! in_array( 'blocks', $node['path'], true ); }, ARRAY_FILTER_USE_BOTH ); } /** * Enqueues the global styles defined via theme.json. * * @since 5.8.0 */ function wp_enqueue_global_styles() { $separate_assets = wp_should_load_separate_core_block_assets(); $is_block_theme = wp_is_block_theme(); $is_classic_theme = ! $is_block_theme; /* * Global styles should be printed in the head when loading all styles combined. * The footer should only be used to print global styles for classic themes with separate core assets enabled. * * See https://core.trac.wordpress.org/ticket/53494. */ if ( ( $is_block_theme && doing_action( 'wp_footer' ) ) || ( $is_classic_theme && doing_action( 'wp_footer' ) && ! $separate_assets ) || ( $is_classic_theme && doing_action( 'wp_enqueue_scripts' ) && $separate_assets ) ) { return; } /* * If loading the CSS for each block separately, then load the theme.json CSS conditionally. * This removes the CSS from the global-styles stylesheet and adds it to the inline CSS for each block. * This filter must be registered before calling wp_get_global_stylesheet(); */ add_filter( 'wp_theme_json_get_style_nodes', 'wp_filter_out_block_nodes' ); $stylesheet = wp_get_global_stylesheet(); if ( empty( $stylesheet ) ) { return; } wp_register_style( 'global-styles', false ); wp_add_inline_style( 'global-styles', $stylesheet ); wp_enqueue_style( 'global-styles' ); // Add each block as an inline css. wp_add_global_styles_for_blocks(); } /** * Enqueues the global styles custom css defined via theme.json. * * @since 6.2.0 */ function wp_enqueue_global_styles_custom_css() { if ( ! wp_is_block_theme() ) { return; } // Don't enqueue Customizer's custom CSS separately. remove_action( 'wp_head', 'wp_custom_css_cb', 101 ); $custom_css = wp_get_custom_css(); $custom_css .= wp_get_global_styles_custom_css(); if ( ! empty( $custom_css ) ) { wp_add_inline_style( 'global-styles', $custom_css ); } } /** * Checks if the editor scripts and styles for all registered block types * should be enqueued on the current screen. * * @since 5.6.0 * * @global WP_Screen $current_screen WordPress current screen object. * * @return bool Whether scripts and styles should be enqueued. */ function wp_should_load_block_editor_scripts_and_styles() { global $current_screen; $is_block_editor_screen = ( $current_screen instanceof WP_Screen ) && $current_screen->is_block_editor(); /** * Filters the flag that decides whether or not block editor scripts and styles * are going to be enqueued on the current screen. * * @since 5.6.0 * * @param bool $is_block_editor_screen Current value of the flag. */ return apply_filters( 'should_load_block_editor_scripts_and_styles', $is_block_editor_screen ); } /** * Checks whether separate styles should be loaded for core blocks on-render. * * When this function returns true, other functions ensure that core blocks * only load their assets on-render, and each block loads its own, individual * assets. Third-party blocks only load their assets when rendered. * * When this function returns false, all core block assets are loaded regardless * of whether they are rendered in a page or not, because they are all part of * the `block-library/style.css` file. Assets for third-party blocks are always * enqueued regardless of whether they are rendered or not. * * This only affects front end and not the block editor screens. * * @see wp_enqueue_registered_block_scripts_and_styles() * @see register_block_style_handle() * * @since 5.8.0 * * @return bool Whether separate assets will be loaded. */ function wp_should_load_separate_core_block_assets() { if ( is_admin() || is_feed() || ( defined( 'REST_REQUEST' ) && REST_REQUEST ) ) { return false; } /** * Filters whether block styles should be loaded separately. * * Returning false loads all core block assets, regardless of whether they are rendered * in a page or not. Returning true loads core block assets only when they are rendered. * * @since 5.8.0 * * @param bool $load_separate_assets Whether separate assets will be loaded. * Default false (all block assets are loaded, even when not used). */ return apply_filters( 'should_load_separate_core_block_assets', false ); } /** * Enqueues registered block scripts and styles, depending on current rendered * context (only enqueuing editor scripts while in context of the editor). * * @since 5.0.0 * * @global WP_Screen $current_screen WordPress current screen object. */ function wp_enqueue_registered_block_scripts_and_styles() { global $current_screen; if ( wp_should_load_separate_core_block_assets() ) { return; } $load_editor_scripts_and_styles = is_admin() && wp_should_load_block_editor_scripts_and_styles(); $block_registry = WP_Block_Type_Registry::get_instance(); foreach ( $block_registry->get_all_registered() as $block_name => $block_type ) { // Front-end and editor styles. foreach ( $block_type->style_handles as $style_handle ) { wp_enqueue_style( $style_handle ); } // Front-end and editor scripts. foreach ( $block_type->script_handles as $script_handle ) { wp_enqueue_script( $script_handle ); } if ( $load_editor_scripts_and_styles ) { // Editor styles. foreach ( $block_type->editor_style_handles as $editor_style_handle ) { wp_enqueue_style( $editor_style_handle ); } // Editor scripts. foreach ( $block_type->editor_script_handles as $editor_script_handle ) { wp_enqueue_script( $editor_script_handle ); } } } } /** * Function responsible for enqueuing the styles required for block styles functionality on the editor and on the frontend. * * @since 5.3.0 * * @global WP_Styles $wp_styles */ function enqueue_block_styles_assets() { global $wp_styles; $block_styles = WP_Block_Styles_Registry::get_instance()->get_all_registered(); foreach ( $block_styles as $block_name => $styles ) { foreach ( $styles as $style_properties ) { if ( isset( $style_properties['style_handle'] ) ) { // If the site loads separate styles per-block, enqueue the stylesheet on render. if ( wp_should_load_separate_core_block_assets() ) { add_filter( 'render_block', static function ( $html, $block ) use ( $block_name, $style_properties ) { if ( $block['blockName'] === $block_name ) { wp_enqueue_style( $style_properties['style_handle'] ); } return $html; }, 10, 2 ); } else { wp_enqueue_style( $style_properties['style_handle'] ); } } if ( isset( $style_properties['inline_style'] ) ) { // Default to "wp-block-library". $handle = 'wp-block-library'; // If the site loads separate styles per-block, check if the block has a stylesheet registered. if ( wp_should_load_separate_core_block_assets() ) { $block_stylesheet_handle = generate_block_asset_handle( $block_name, 'style' ); if ( isset( $wp_styles->registered[ $block_stylesheet_handle ] ) ) { $handle = $block_stylesheet_handle; } } // Add inline styles to the calculated handle. wp_add_inline_style( $handle, $style_properties['inline_style'] ); } } } } /** * Function responsible for enqueuing the assets required for block styles functionality on the editor. * * @since 5.3.0 */ function enqueue_editor_block_styles_assets() { $block_styles = WP_Block_Styles_Registry::get_instance()->get_all_registered(); $register_script_lines = array( '( function() {' ); foreach ( $block_styles as $block_name => $styles ) { foreach ( $styles as $style_properties ) { $block_style = array( 'name' => $style_properties['name'], 'label' => $style_properties['label'], ); if ( isset( $style_properties['is_default'] ) ) { $block_style['isDefault'] = $style_properties['is_default']; } $register_script_lines[] = sprintf( ' wp.blocks.registerBlockStyle( \'%s\', %s );', $block_name, wp_json_encode( $block_style ) ); } } $register_script_lines[] = '} )();'; $inline_script = implode( "\n", $register_script_lines ); wp_register_script( 'wp-block-styles', false, array( 'wp-blocks' ), true, array( 'in_footer' => true ) ); wp_add_inline_script( 'wp-block-styles', $inline_script ); wp_enqueue_script( 'wp-block-styles' ); } /** * Enqueues the assets required for the block directory within the block editor. * * @since 5.5.0 */ function wp_enqueue_editor_block_directory_assets() { wp_enqueue_script( 'wp-block-directory' ); wp_enqueue_style( 'wp-block-directory' ); } /** * Enqueues the assets required for the format library within the block editor. * * @since 5.8.0 */ function wp_enqueue_editor_format_library_assets() { wp_enqueue_script( 'wp-format-library' ); wp_enqueue_style( 'wp-format-library' ); } /** * Sanitizes an attributes array into an attributes string to be placed inside a `\n", wp_sanitize_script_attributes( $attributes ) ); } /** * Prints formatted ` * * In an HTML document this would print "…" to the console, * but in an XHTML document it would print "…" to the console. * * * * In an HTML document this would print "An image is in HTML", * but it's an invalid XHTML document because it interprets the `` * as an empty tag missing its closing `/`. * * @see https://www.w3.org/TR/xhtml1/#h-4.8 */ if ( ! $is_html5 ) { /* * If the string `]]>` exists within the JavaScript it would break * out of any wrapping CDATA section added here, so to start, it's * necessary to escape that sequence which requires splitting the * content into two CDATA sections wherever it's found. * * Note: it's only necessary to escape the closing `]]>` because * an additional `', ']]]]>', $javascript ); // Wrap the entire escaped script inside a CDATA section. $javascript = sprintf( "/* */", $javascript ); } $javascript = "\n" . trim( $javascript, "\n\r " ) . "\n"; /** * Filters attributes to be added to a script tag. * * @since 5.7.0 * * @param array $attributes Key-value pairs representing `\n", wp_sanitize_script_attributes( $attributes ), $javascript ); } /** * Prints inline JavaScript wrapped in `" from * around an inline script after trimming whitespace. Typlically this * is used in conjunction with output buffering, where `ob_get_clean()` * is passed as the `$contents` argument. * * Example: * * // Strips exact literal empty SCRIPT tags. * $js = '; * 'sayHello();' === wp_remove_surrounding_empty_script_tags( $js ); * * // Otherwise if anything is different it warns in the JS console. * $js = ''; * 'console.error( ... )' === wp_remove_surrounding_empty_script_tags( $js ); * * @private * @since 6.4.0 * * @see wp_print_inline_script_tag() * @see wp_get_inline_script_tag() * * @param string $contents Script body with manually created SCRIPT tag literals. * @return string Script body without surrounding script tag literals, or * original contents if both exact literals aren't present. */ function wp_remove_surrounding_empty_script_tags( $contents ) { $contents = trim( $contents ); $opener = ''; if ( strlen( $contents ) > strlen( $opener ) + strlen( $closer ) && strtoupper( substr( $contents, 0, strlen( $opener ) ) ) === $opener && strtoupper( substr( $contents, -strlen( $closer ) ) ) === $closer ) { return substr( $contents, strlen( $opener ), -strlen( $closer ) ); } else { $error_message = __( 'Expected string to start with script tag (without attributes) and end with script tag, with optional whitespace.' ); _doing_it_wrong( __FUNCTION__, $error_message, '6.4' ); return sprintf( 'console.error(%s)', wp_json_encode( __( 'Function wp_remove_surrounding_empty_script_tags() used incorrectly in PHP.' ) . ' ' . $error_message ) ); } } Filters arguments for retrieving object terms. * * @since 4.9.0 * * @param array $args An array of arguments for retrieving terms for the given object(s). * See {@see wp_get_object_terms()} for details. * @param int[] $object_ids Array of object IDs. * @param string[] $taxonomies Array of taxonomy names to retrieve terms from. */ $args = apply_filters( 'wp_get_object_terms_args', $args, $object_ids, $taxonomies ); /* * When one or more queried taxonomies is registered with an 'args' array, * those params override the `$args` passed to this function. */ $terms = array(); if ( count( $taxonomies ) > 1 ) { foreach ( $taxonomies as $index => $taxonomy ) { $t = get_taxonomy( $taxonomy ); if ( isset( $t->args ) && is_array( $t->args ) && array_merge( $args, $t->args ) != $args ) { unset( $taxonomies[ $index ] ); $terms = array_merge( $terms, wp_get_object_terms( $object_ids, $taxonomy, array_merge( $args, $t->args ) ) ); } } } else { $t = get_taxonomy( $taxonomies[0] ); if ( isset( $t->args ) && is_array( $t->args ) ) { $args = array_merge( $args, $t->args ); } } $args['taxonomy'] = $taxonomies; $args['object_ids'] = $object_ids; // Taxonomies registered without an 'args' param are handled here. if ( ! empty( $taxonomies ) ) { $terms_from_remaining_taxonomies = get_terms( $args ); // Array keys should be preserved for values of $fields that use term_id for keys. if ( ! empty( $args['fields'] ) && str_starts_with( $args['fields'], 'id=>' ) ) { $terms = $terms + $terms_from_remaining_taxonomies; } else { $terms = array_merge( $terms, $terms_from_remaining_taxonomies ); } } /** * Filters the terms for a given object or objects. * * @since 4.2.0 * * @param WP_Term[]|int[]|string[]|string $terms Array of terms or a count thereof as a numeric string. * @param int[] $object_ids Array of object IDs for which terms were retrieved. * @param string[] $taxonomies Array of taxonomy names from which terms were retrieved. * @param array $args Array of arguments for retrieving terms for the given * object(s). See wp_get_object_terms() for details. */ $terms = apply_filters( 'get_object_terms', $terms, $object_ids, $taxonomies, $args ); $object_ids = implode( ',', $object_ids ); $taxonomies = "'" . implode( "', '", array_map( 'esc_sql', $taxonomies ) ) . "'"; /** * Filters the terms for a given object or objects. * * The `$taxonomies` parameter passed to this filter is formatted as a SQL fragment. The * {@see 'get_object_terms'} filter is recommended as an alternative. * * @since 2.8.0 * * @param WP_Term[]|int[]|string[]|string $terms Array of terms or a count thereof as a numeric string. * @param string $object_ids Comma separated list of object IDs for which terms were retrieved. * @param string $taxonomies SQL fragment of taxonomy names from which terms were retrieved. * @param array $args Array of arguments for retrieving terms for the given * object(s). See wp_get_object_terms() for details. */ return apply_filters( 'wp_get_object_terms', $terms, $object_ids, $taxonomies, $args ); } /** * Adds a new term to the database. * * A non-existent term is inserted in the following sequence: * 1. The term is added to the term table, then related to the taxonomy. * 2. If everything is correct, several actions are fired. * 3. The 'term_id_filter' is evaluated. * 4. The term cache is cleaned. * 5. Several more actions are fired. * 6. An array is returned containing the `term_id` and `term_taxonomy_id`. * * If the 'slug' argument is not empty, then it is checked to see if the term * is invalid. If it is not a valid, existing term, it is added and the term_id * is given. * * If the taxonomy is hierarchical, and the 'parent' argument is not empty, * the term is inserted and the term_id will be given. * * Error handling: * If `$taxonomy` does not exist or `$term` is empty, * a WP_Error object will be returned. * * If the term already exists on the same hierarchical level, * or the term slug and name are not unique, a WP_Error object will be returned. * * @global wpdb $wpdb WordPress database abstraction object. * * @since 2.3.0 * * @param string $term The term name to add. * @param string $taxonomy The taxonomy to which to add the term. * @param array|string $args { * Optional. Array or query string of arguments for inserting a term. * * @type string $alias_of Slug of the term to make this term an alias of. * Default empty string. Accepts a term slug. * @type string $description The term description. Default empty string. * @type int $parent The id of the parent term. Default 0. * @type string $slug The term slug to use. Default empty string. * } * @return array|WP_Error { * An array of the new term data, WP_Error otherwise. * * @type int $term_id The new term ID. * @type int|string $term_taxonomy_id The new term taxonomy ID. Can be a numeric string. * } */ function wp_insert_term( $term, $taxonomy, $args = array() ) { global $wpdb; if ( ! taxonomy_exists( $taxonomy ) ) { return new WP_Error( 'invalid_taxonomy', __( 'Invalid taxonomy.' ) ); } /** * Filters a term before it is sanitized and inserted into the database. * * @since 3.0.0 * @since 6.1.0 The `$args` parameter was added. * * @param string|WP_Error $term The term name to add, or a WP_Error object if there's an error. * @param string $taxonomy Taxonomy slug. * @param array|string $args Array or query string of arguments passed to wp_insert_term(). */ $term = apply_filters( 'pre_insert_term', $term, $taxonomy, $args ); if ( is_wp_error( $term ) ) { return $term; } if ( is_int( $term ) && 0 === $term ) { return new WP_Error( 'invalid_term_id', __( 'Invalid term ID.' ) ); } if ( '' === trim( $term ) ) { return new WP_Error( 'empty_term_name', __( 'A name is required for this term.' ) ); } $defaults = array( 'alias_of' => '', 'description' => '', 'parent' => 0, 'slug' => '', ); $args = wp_parse_args( $args, $defaults ); if ( (int) $args['parent'] > 0 && ! term_exists( (int) $args['parent'] ) ) { return new WP_Error( 'missing_parent', __( 'Parent term does not exist.' ) ); } $args['name'] = $term; $args['taxonomy'] = $taxonomy; // Coerce null description to strings, to avoid database errors. $args['description'] = (string) $args['description']; $args = sanitize_term( $args, $taxonomy, 'db' ); // expected_slashed ($name) $name = wp_unslash( $args['name'] ); $description = wp_unslash( $args['description'] ); $parent = (int) $args['parent']; $slug_provided = ! empty( $args['slug'] ); if ( ! $slug_provided ) { $slug = sanitize_title( $name ); } else { $slug = $args['slug']; } $term_group = 0; if ( $args['alias_of'] ) { $alias = get_term_by( 'slug', $args['alias_of'], $taxonomy ); if ( ! empty( $alias->term_group ) ) { // The alias we want is already in a group, so let's use that one. $term_group = $alias->term_group; } elseif ( ! empty( $alias->term_id ) ) { /* * The alias is not in a group, so we create a new one * and add the alias to it. */ $term_group = $wpdb->get_var( "SELECT MAX(term_group) FROM $wpdb->terms" ) + 1; wp_update_term( $alias->term_id, $taxonomy, array( 'term_group' => $term_group, ) ); } } /* * Prevent the creation of terms with duplicate names at the same level of a taxonomy hierarchy, * unless a unique slug has been explicitly provided. */ $name_matches = get_terms( array( 'taxonomy' => $taxonomy, 'name' => $name, 'hide_empty' => false, 'parent' => $args['parent'], 'update_term_meta_cache' => false, ) ); /* * The `name` match in `get_terms()` doesn't differentiate accented characters, * so we do a stricter comparison here. */ $name_match = null; if ( $name_matches ) { foreach ( $name_matches as $_match ) { if ( strtolower( $name ) === strtolower( $_match->name ) ) { $name_match = $_match; break; } } } if ( $name_match ) { $slug_match = get_term_by( 'slug', $slug, $taxonomy ); if ( ! $slug_provided || $name_match->slug === $slug || $slug_match ) { if ( is_taxonomy_hierarchical( $taxonomy ) ) { $siblings = get_terms( array( 'taxonomy' => $taxonomy, 'get' => 'all', 'parent' => $parent, 'update_term_meta_cache' => false, ) ); $existing_term = null; $sibling_names = wp_list_pluck( $siblings, 'name' ); $sibling_slugs = wp_list_pluck( $siblings, 'slug' ); if ( ( ! $slug_provided || $name_match->slug === $slug ) && in_array( $name, $sibling_names, true ) ) { $existing_term = $name_match; } elseif ( $slug_match && in_array( $slug, $sibling_slugs, true ) ) { $existing_term = $slug_match; } if ( $existing_term ) { return new WP_Error( 'term_exists', __( 'A term with the name provided already exists with this parent.' ), $existing_term->term_id ); } } else { return new WP_Error( 'term_exists', __( 'A term with the name provided already exists in this taxonomy.' ), $name_match->term_id ); } } } $slug = wp_unique_term_slug( $slug, (object) $args ); $data = compact( 'name', 'slug', 'term_group' ); /** * Filters term data before it is inserted into the database. * * @since 4.7.0 * * @param array $data Term data to be inserted. * @param string $taxonomy Taxonomy slug. * @param array $args Arguments passed to wp_insert_term(). */ $data = apply_filters( 'wp_insert_term_data', $data, $taxonomy, $args ); if ( false === $wpdb->insert( $wpdb->terms, $data ) ) { return new WP_Error( 'db_insert_error', __( 'Could not insert term into the database.' ), $wpdb->last_error ); } $term_id = (int) $wpdb->insert_id; // Seems unreachable. However, is used in the case that a term name is provided, which sanitizes to an empty string. if ( empty( $slug ) ) { $slug = sanitize_title( $slug, $term_id ); /** This action is documented in wp-includes/taxonomy.php */ do_action( 'edit_terms', $term_id, $taxonomy ); $wpdb->update( $wpdb->terms, compact( 'slug' ), compact( 'term_id' ) ); /** This action is documented in wp-includes/taxonomy.php */ do_action( 'edited_terms', $term_id, $taxonomy ); } $tt_id = $wpdb->get_var( $wpdb->prepare( "SELECT tt.term_taxonomy_id FROM $wpdb->term_taxonomy AS tt INNER JOIN $wpdb->terms AS t ON tt.term_id = t.term_id WHERE tt.taxonomy = %s AND t.term_id = %d", $taxonomy, $term_id ) ); if ( ! empty( $tt_id ) ) { return array( 'term_id' => $term_id, 'term_taxonomy_id' => $tt_id, ); } if ( false === $wpdb->insert( $wpdb->term_taxonomy, compact( 'term_id', 'taxonomy', 'description', 'parent' ) + array( 'count' => 0 ) ) ) { return new WP_Error( 'db_insert_error', __( 'Could not insert term taxonomy into the database.' ), $wpdb->last_error ); } $tt_id = (int) $wpdb->insert_id; /* * Sanity check: if we just created a term with the same parent + taxonomy + slug but a higher term_id than * an existing term, then we have unwittingly created a duplicate term. Delete the dupe, and use the term_id * and term_taxonomy_id of the older term instead. Then return out of the function so that the "create" hooks * are not fired. */ $duplicate_term = $wpdb->get_row( $wpdb->prepare( "SELECT t.term_id, t.slug, tt.term_taxonomy_id, tt.taxonomy FROM $wpdb->terms AS t INNER JOIN $wpdb->term_taxonomy AS tt ON ( tt.term_id = t.term_id ) WHERE t.slug = %s AND tt.parent = %d AND tt.taxonomy = %s AND t.term_id < %d AND tt.term_taxonomy_id != %d", $slug, $parent, $taxonomy, $term_id, $tt_id ) ); /** * Filters the duplicate term check that takes place during term creation. * * Term parent + taxonomy + slug combinations are meant to be unique, and wp_insert_term() * performs a last-minute confirmation of this uniqueness before allowing a new term * to be created. Plugins with different uniqueness requirements may use this filter * to bypass or modify the duplicate-term check. * * @since 5.1.0 * * @param object $duplicate_term Duplicate term row from terms table, if found. * @param string $term Term being inserted. * @param string $taxonomy Taxonomy name. * @param array $args Arguments passed to wp_insert_term(). * @param int $tt_id term_taxonomy_id for the newly created term. */ $duplicate_term = apply_filters( 'wp_insert_term_duplicate_term_check', $duplicate_term, $term, $taxonomy, $args, $tt_id ); if ( $duplicate_term ) { $wpdb->delete( $wpdb->terms, array( 'term_id' => $term_id ) ); $wpdb->delete( $wpdb->term_taxonomy, array( 'term_taxonomy_id' => $tt_id ) ); $term_id = (int) $duplicate_term->term_id; $tt_id = (int) $duplicate_term->term_taxonomy_id; clean_term_cache( $term_id, $taxonomy ); return array( 'term_id' => $term_id, 'term_taxonomy_id' => $tt_id, ); } /** * Fires immediately after a new term is created, before the term cache is cleaned. * * The {@see 'create_$taxonomy'} hook is also available for targeting a specific * taxonomy. * * @since 2.3.0 * @since 6.1.0 The `$args` parameter was added. * * @param int $term_id Term ID. * @param int $tt_id Term taxonomy ID. * @param string $taxonomy Taxonomy slug. * @param array $args Arguments passed to wp_insert_term(). */ do_action( 'create_term', $term_id, $tt_id, $taxonomy, $args ); /** * Fires after a new term is created for a specific taxonomy. * * The dynamic portion of the hook name, `$taxonomy`, refers * to the slug of the taxonomy the term was created for. * * Possible hook names include: * * - `create_category` * - `create_post_tag` * * @since 2.3.0 * @since 6.1.0 The `$args` parameter was added. * * @param int $term_id Term ID. * @param int $tt_id Term taxonomy ID. * @param array $args Arguments passed to wp_insert_term(). */ do_action( "create_{$taxonomy}", $term_id, $tt_id, $args ); /** * Filters the term ID after a new term is created. * * @since 2.3.0 * @since 6.1.0 The `$args` parameter was added. * * @param int $term_id Term ID. * @param int $tt_id Term taxonomy ID. * @param array $args Arguments passed to wp_insert_term(). */ $term_id = apply_filters( 'term_id_filter', $term_id, $tt_id, $args ); clean_term_cache( $term_id, $taxonomy ); /** * Fires after a new term is created, and after the term cache has been cleaned. * * The {@see 'created_$taxonomy'} hook is also available for targeting a specific * taxonomy. * * @since 2.3.0 * @since 6.1.0 The `$args` parameter was added. * * @param int $term_id Term ID. * @param int $tt_id Term taxonomy ID. * @param string $taxonomy Taxonomy slug. * @param array $args Arguments passed to wp_insert_term(). */ do_action( 'created_term', $term_id, $tt_id, $taxonomy, $args ); /** * Fires after a new term in a specific taxonomy is created, and after the term * cache has been cleaned. * * The dynamic portion of the hook name, `$taxonomy`, refers to the taxonomy slug. * * Possible hook names include: * * - `created_category` * - `created_post_tag` * * @since 2.3.0 * @since 6.1.0 The `$args` parameter was added. * * @param int $term_id Term ID. * @param int $tt_id Term taxonomy ID. * @param array $args Arguments passed to wp_insert_term(). */ do_action( "created_{$taxonomy}", $term_id, $tt_id, $args ); /** * Fires after a term has been saved, and the term cache has been cleared. * * The {@see 'saved_$taxonomy'} hook is also available for targeting a specific * taxonomy. * * @since 5.5.0 * @since 6.1.0 The `$args` parameter was added. * * @param int $term_id Term ID. * @param int $tt_id Term taxonomy ID. * @param string $taxonomy Taxonomy slug. * @param bool $update Whether this is an existing term being updated. * @param array $args Arguments passed to wp_insert_term(). */ do_action( 'saved_term', $term_id, $tt_id, $taxonomy, false, $args ); /** * Fires after a term in a specific taxonomy has been saved, and the term * cache has been cleared. * * The dynamic portion of the hook name, `$taxonomy`, refers to the taxonomy slug. * * Possible hook names include: * * - `saved_category` * - `saved_post_tag` * * @since 5.5.0 * @since 6.1.0 The `$args` parameter was added. * * @param int $term_id Term ID. * @param int $tt_id Term taxonomy ID. * @param bool $update Whether this is an existing term being updated. * @param array $args Arguments passed to wp_insert_term(). */ do_action( "saved_{$taxonomy}", $term_id, $tt_id, false, $args ); return array( 'term_id' => $term_id, 'term_taxonomy_id' => $tt_id, ); } /** * Creates term and taxonomy relationships. * * Relates an object (post, link, etc.) to a term and taxonomy type. Creates the * term and taxonomy relationship if it doesn't already exist. Creates a term if * it doesn't exist (using the slug). * * A relationship means that the term is grouped in or belongs to the taxonomy. * A term has no meaning until it is given context by defining which taxonomy it * exists under. * * @since 2.3.0 * * @global wpdb $wpdb WordPress database abstraction object. * * @param int $object_id The object to relate to. * @param string|int|array $terms A single term slug, single term ID, or array of either term slugs or IDs. * Will replace all existing related terms in this taxonomy. Passing an * empty array will remove all related terms. * @param string $taxonomy The context in which to relate the term to the object. * @param bool $append Optional. If false will delete difference of terms. Default false. * @return array|WP_Error Term taxonomy IDs of the affected terms or WP_Error on failure. */ function wp_set_object_terms( $object_id, $terms, $taxonomy, $append = false ) { global $wpdb; $object_id = (int) $object_id; if ( ! taxonomy_exists( $taxonomy ) ) { return new WP_Error( 'invalid_taxonomy', __( 'Invalid taxonomy.' ) ); } if ( empty( $terms ) ) { $terms = array(); } elseif ( ! is_array( $terms ) ) { $terms = array( $terms ); } if ( ! $append ) { $old_tt_ids = wp_get_object_terms( $object_id, $taxonomy, array( 'fields' => 'tt_ids', 'orderby' => 'none', 'update_term_meta_cache' => false, ) ); } else { $old_tt_ids = array(); } $tt_ids = array(); $term_ids = array(); $new_tt_ids = array(); foreach ( (array) $terms as $term ) { if ( '' === trim( $term ) ) { continue; } $term_info = term_exists( $term, $taxonomy ); if ( ! $term_info ) { // Skip if a non-existent term ID is passed. if ( is_int( $term ) ) { continue; } $term_info = wp_insert_term( $term, $taxonomy ); } if ( is_wp_error( $term_info ) ) { return $term_info; } $term_ids[] = $term_info['term_id']; $tt_id = $term_info['term_taxonomy_id']; $tt_ids[] = $tt_id; if ( $wpdb->get_var( $wpdb->prepare( "SELECT term_taxonomy_id FROM $wpdb->term_relationships WHERE object_id = %d AND term_taxonomy_id = %d", $object_id, $tt_id ) ) ) { continue; } /** * Fires immediately before an object-term relationship is added. * * @since 2.9.0 * @since 4.7.0 Added the `$taxonomy` parameter. * * @param int $object_id Object ID. * @param int $tt_id Term taxonomy ID. * @param string $taxonomy Taxonomy slug. */ do_action( 'add_term_relationship', $object_id, $tt_id, $taxonomy ); $wpdb->insert( $wpdb->term_relationships, array( 'object_id' => $object_id, 'term_taxonomy_id' => $tt_id, ) ); /** * Fires immediately after an object-term relationship is added. * * @since 2.9.0 * @since 4.7.0 Added the `$taxonomy` parameter. * * @param int $object_id Object ID. * @param int $tt_id Term taxonomy ID. * @param string $taxonomy Taxonomy slug. */ do_action( 'added_term_relationship', $object_id, $tt_id, $taxonomy ); $new_tt_ids[] = $tt_id; } if ( $new_tt_ids ) { wp_update_term_count( $new_tt_ids, $taxonomy ); } if ( ! $append ) { $delete_tt_ids = array_diff( $old_tt_ids, $tt_ids ); if ( $delete_tt_ids ) { $in_delete_tt_ids = "'" . implode( "', '", $delete_tt_ids ) . "'"; $delete_term_ids = $wpdb->get_col( $wpdb->prepare( "SELECT tt.term_id FROM $wpdb->term_taxonomy AS tt WHERE tt.taxonomy = %s AND tt.term_taxonomy_id IN ($in_delete_tt_ids)", $taxonomy ) ); $delete_term_ids = array_map( 'intval', $delete_term_ids ); $remove = wp_remove_object_terms( $object_id, $delete_term_ids, $taxonomy ); if ( is_wp_error( $remove ) ) { return $remove; } } } $t = get_taxonomy( $taxonomy ); if ( ! $append && isset( $t->sort ) && $t->sort ) { $values = array(); $term_order = 0; $final_tt_ids = wp_get_object_terms( $object_id, $taxonomy, array( 'fields' => 'tt_ids', 'update_term_meta_cache' => false, ) ); foreach ( $tt_ids as $tt_id ) { if ( in_array( (int) $tt_id, $final_tt_ids, true ) ) { $values[] = $wpdb->prepare( '(%d, %d, %d)', $object_id, $tt_id, ++$term_order ); } } if ( $values ) { if ( false === $wpdb->query( "INSERT INTO $wpdb->term_relationships (object_id, term_taxonomy_id, term_order) VALUES " . implode( ',', $values ) . ' ON DUPLICATE KEY UPDATE term_order = VALUES(term_order)' ) ) { return new WP_Error( 'db_insert_error', __( 'Could not insert term relationship into the database.' ), $wpdb->last_error ); } } } wp_cache_delete( $object_id, $taxonomy . '_relationships' ); wp_cache_set_terms_last_changed(); /** * Fires after an object's terms have been set. * * @since 2.8.0 * * @param int $object_id Object ID. * @param array $terms An array of object term IDs or slugs. * @param array $tt_ids An array of term taxonomy IDs. * @param string $taxonomy Taxonomy slug. * @param bool $append Whether to append new terms to the old terms. * @param array $old_tt_ids Old array of term taxonomy IDs. */ do_action( 'set_object_terms', $object_id, $terms, $tt_ids, $taxonomy, $append, $old_tt_ids ); return $tt_ids; } /** * Adds term(s) associated with a given object. * * @since 3.6.0 * * @param int $object_id The ID of the object to which the terms will be added. * @param string|int|array $terms The slug(s) or ID(s) of the term(s) to add. * @param array|string $taxonomy Taxonomy name. * @return array|WP_Error Term taxonomy IDs of the affected terms. */ function wp_add_object_terms( $object_id, $terms, $taxonomy ) { return wp_set_object_terms( $object_id, $terms, $taxonomy, true ); } /** * Removes term(s) associated with a given object. * * @since 3.6.0 * * @global wpdb $wpdb WordPress database abstraction object. * * @param int $object_id The ID of the object from which the terms will be removed. * @param string|int|array $terms The slug(s) or ID(s) of the term(s) to remove. * @param string $taxonomy Taxonomy name. * @return bool|WP_Error True on success, false or WP_Error on failure. */ function wp_remove_object_terms( $object_id, $terms, $taxonomy ) { global $wpdb; $object_id = (int) $object_id; if ( ! taxonomy_exists( $taxonomy ) ) { return new WP_Error( 'invalid_taxonomy', __( 'Invalid taxonomy.' ) ); } if ( ! is_array( $terms ) ) { $terms = array( $terms ); } $tt_ids = array(); foreach ( (array) $terms as $term ) { if ( '' === trim( $term ) ) { continue; } $term_info = term_exists( $term, $taxonomy ); if ( ! $term_info ) { // Skip if a non-existent term ID is passed. if ( is_int( $term ) ) { continue; } } if ( is_wp_error( $term_info ) ) { return $term_info; } $tt_ids[] = $term_info['term_taxonomy_id']; } if ( $tt_ids ) { $in_tt_ids = "'" . implode( "', '", $tt_ids ) . "'"; /** * Fires immediately before an object-term relationship is deleted. * * @since 2.9.0 * @since 4.7.0 Added the `$taxonomy` parameter. * * @param int $object_id Object ID. * @param array $tt_ids An array of term taxonomy IDs. * @param string $taxonomy Taxonomy slug. */ do_action( 'delete_term_relationships', $object_id, $tt_ids, $taxonomy ); $deleted = $wpdb->query( $wpdb->prepare( "DELETE FROM $wpdb->term_relationships WHERE object_id = %d AND term_taxonomy_id IN ($in_tt_ids)", $object_id ) ); wp_cache_delete( $object_id, $taxonomy . '_relationships' ); wp_cache_set_terms_last_changed(); /** * Fires immediately after an object-term relationship is deleted. * * @since 2.9.0 * @since 4.7.0 Added the `$taxonomy` parameter. * * @param int $object_id Object ID. * @param array $tt_ids An array of term taxonomy IDs. * @param string $taxonomy Taxonomy slug. */ do_action( 'deleted_term_relationships', $object_id, $tt_ids, $taxonomy ); wp_update_term_count( $tt_ids, $taxonomy ); return (bool) $deleted; } return false; } /** * Makes term slug unique, if it isn't already. * * The `$slug` has to be unique global to every taxonomy, meaning that one * taxonomy term can't have a matching slug with another taxonomy term. Each * slug has to be globally unique for every taxonomy. * * The way this works is that if the taxonomy that the term belongs to is * hierarchical and has a parent, it will append that parent to the $slug. * * If that still doesn't return a unique slug, then it tries to append a number * until it finds a number that is truly unique. * * The only purpose for `$term` is for appending a parent, if one exists. * * @since 2.3.0 * * @global wpdb $wpdb WordPress database abstraction object. * * @param string $slug The string that will be tried for a unique slug. * @param object $term The term object that the `$slug` will belong to. * @return string Will return a true unique slug. */ function wp_unique_term_slug( $slug, $term ) { global $wpdb; $needs_suffix = true; $original_slug = $slug; // As of 4.1, duplicate slugs are allowed as long as they're in different taxonomies. if ( ! term_exists( $slug ) || get_option( 'db_version' ) >= 30133 && ! get_term_by( 'slug', $slug, $term->taxonomy ) ) { $needs_suffix = false; } /* * If the taxonomy supports hierarchy and the term has a parent, make the slug unique * by incorporating parent slugs. */ $parent_suffix = ''; if ( $needs_suffix && is_taxonomy_hierarchical( $term->taxonomy ) && ! empty( $term->parent ) ) { $the_parent = $term->parent; while ( ! empty( $the_parent ) ) { $parent_term = get_term( $the_parent, $term->taxonomy ); if ( is_wp_error( $parent_term ) || empty( $parent_term ) ) { break; } $parent_suffix .= '-' . $parent_term->slug; if ( ! term_exists( $slug . $parent_suffix ) ) { break; } if ( empty( $parent_term->parent ) ) { break; } $the_parent = $parent_term->parent; } } // If we didn't get a unique slug, try appending a number to make it unique. /** * Filters whether the proposed unique term slug is bad. * * @since 4.3.0 * * @param bool $needs_suffix Whether the slug needs to be made unique with a suffix. * @param string $slug The slug. * @param object $term Term object. */ if ( apply_filters( 'wp_unique_term_slug_is_bad_slug', $needs_suffix, $slug, $term ) ) { if ( $parent_suffix ) { $slug .= $parent_suffix; } if ( ! empty( $term->term_id ) ) { $query = $wpdb->prepare( "SELECT slug FROM $wpdb->terms WHERE slug = %s AND term_id != %d", $slug, $term->term_id ); } else { $query = $wpdb->prepare( "SELECT slug FROM $wpdb->terms WHERE slug = %s", $slug ); } if ( $wpdb->get_var( $query ) ) { // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared $num = 2; do { $alt_slug = $slug . "-$num"; ++$num; $slug_check = $wpdb->get_var( $wpdb->prepare( "SELECT slug FROM $wpdb->terms WHERE slug = %s", $alt_slug ) ); } while ( $slug_check ); $slug = $alt_slug; } } /** * Filters the unique term slug. * * @since 4.3.0 * * @param string $slug Unique term slug. * @param object $term Term object. * @param string $original_slug Slug originally passed to the function for testing. */ return apply_filters( 'wp_unique_term_slug', $slug, $term, $original_slug ); } /** * Updates term based on arguments provided. * * The `$args` will indiscriminately override all values with the same field name. * Care must be taken to not override important information need to update or * update will fail (or perhaps create a new term, neither would be acceptable). * * Defaults will set 'alias_of', 'description', 'parent', and 'slug' if not * defined in `$args` already. * * 'alias_of' will create a term group, if it doesn't already exist, and * update it for the `$term`. * * If the 'slug' argument in `$args` is missing, then the 'name' will be used. * If you set 'slug' and it isn't unique, then a WP_Error is returned. * If you don't pass any slug, then a unique one will be created. * * @since 2.3.0 * * @global wpdb $wpdb WordPress database abstraction object. * * @param int $term_id The ID of the term. * @param string $taxonomy The taxonomy of the term. * @param array $args { * Optional. Array of arguments for updating a term. * * @type string $alias_of Slug of the term to make this term an alias of. * Default empty string. Accepts a term slug. * @type string $description The term description. Default empty string. * @type int $parent The id of the parent term. Default 0. * @type string $slug The term slug to use. Default empty string. * } * @return array|WP_Error An array containing the `term_id` and `term_taxonomy_id`, * WP_Error otherwise. */ function wp_update_term( $term_id, $taxonomy, $args = array() ) { global $wpdb; if ( ! taxonomy_exists( $taxonomy ) ) { return new WP_Error( 'invalid_taxonomy', __( 'Invalid taxonomy.' ) ); } $term_id = (int) $term_id; // First, get all of the original args. $term = get_term( $term_id, $taxonomy ); if ( is_wp_error( $term ) ) { return $term; } if ( ! $term ) { return new WP_Error( 'invalid_term', __( 'Empty Term.' ) ); } $term = (array) $term->data; // Escape data pulled from DB. $term = wp_slash( $term ); // Merge old and new args with new args overwriting old ones. $args = array_merge( $term, $args ); $defaults = array( 'alias_of' => '', 'description' => '', 'parent' => 0, 'slug' => '', ); $args = wp_parse_args( $args, $defaults ); $args = sanitize_term( $args, $taxonomy, 'db' ); $parsed_args = $args; // expected_slashed ($name) $name = wp_unslash( $args['name'] ); $description = wp_unslash( $args['description'] ); $parsed_args['name'] = $name; $parsed_args['description'] = $description; if ( '' === trim( $name ) ) { return new WP_Error( 'empty_term_name', __( 'A name is required for this term.' ) ); } if ( (int) $parsed_args['parent'] > 0 && ! term_exists( (int) $parsed_args['parent'] ) ) { return new WP_Error( 'missing_parent', __( 'Parent term does not exist.' ) ); } $empty_slug = false; if ( empty( $args['slug'] ) ) { $empty_slug = true; $slug = sanitize_title( $name ); } else { $slug = $args['slug']; } $parsed_args['slug'] = $slug; $term_group = isset( $parsed_args['term_group'] ) ? $parsed_args['term_group'] : 0; if ( $args['alias_of'] ) { $alias = get_term_by( 'slug', $args['alias_of'], $taxonomy ); if ( ! empty( $alias->term_group ) ) { // The alias we want is already in a group, so let's use that one. $term_group = $alias->term_group; } elseif ( ! empty( $alias->term_id ) ) { /* * The alias is not in a group, so we create a new one * and add the alias to it. */ $term_group = $wpdb->get_var( "SELECT MAX(term_group) FROM $wpdb->terms" ) + 1; wp_update_term( $alias->term_id, $taxonomy, array( 'term_group' => $term_group, ) ); } $parsed_args['term_group'] = $term_group; } /** * Filters the term parent. * * Hook to this filter to see if it will cause a hierarchy loop. * * @since 3.1.0 * * @param int $parent_term ID of the parent term. * @param int $term_id Term ID. * @param string $taxonomy Taxonomy slug. * @param array $parsed_args An array of potentially altered update arguments for the given term. * @param array $args Arguments passed to wp_update_term(). */ $parent = (int) apply_filters( 'wp_update_term_parent', $args['parent'], $term_id, $taxonomy, $parsed_args, $args ); // Check for duplicate slug. $duplicate = get_term_by( 'slug', $slug, $taxonomy ); if ( $duplicate && $duplicate->term_id !== $term_id ) { /* * If an empty slug was passed or the parent changed, reset the slug to something unique. * Otherwise, bail. */ if ( $empty_slug || ( $parent !== (int) $term['parent'] ) ) { $slug = wp_unique_term_slug( $slug, (object) $args ); } else { /* translators: %s: Taxonomy term slug. */ return new WP_Error( 'duplicate_term_slug', sprintf( __( 'The slug “%s” is already in use by another term.' ), $slug ) ); } } $tt_id = (int) $wpdb->get_var( $wpdb->prepare( "SELECT tt.term_taxonomy_id FROM $wpdb->term_taxonomy AS tt INNER JOIN $wpdb->terms AS t ON tt.term_id = t.term_id WHERE tt.taxonomy = %s AND t.term_id = %d", $taxonomy, $term_id ) ); // Check whether this is a shared term that needs splitting. $_term_id = _split_shared_term( $term_id, $tt_id ); if ( ! is_wp_error( $_term_id ) ) { $term_id = $_term_id; } /** * Fires immediately before the given terms are edited. * * @since 2.9.0 * @since 6.1.0 The `$args` parameter was added. * * @param int $term_id Term ID. * @param string $taxonomy Taxonomy slug. * @param array $args Arguments passed to wp_update_term(). */ do_action( 'edit_terms', $term_id, $taxonomy, $args ); $data = compact( 'name', 'slug', 'term_group' ); /** * Filters term data before it is updated in the database. * * @since 4.7.0 * * @param array $data Term data to be updated. * @param int $term_id Term ID. * @param string $taxonomy Taxonomy slug. * @param array $args Arguments passed to wp_update_term(). */ $data = apply_filters( 'wp_update_term_data', $data, $term_id, $taxonomy, $args ); $wpdb->update( $wpdb->terms, $data, compact( 'term_id' ) ); if ( empty( $slug ) ) { $slug = sanitize_title( $name, $term_id ); $wpdb->update( $wpdb->terms, compact( 'slug' ), compact( 'term_id' ) ); } /** * Fires immediately after a term is updated in the database, but before its * term-taxonomy relationship is updated. * * @since 2.9.0 * @since 6.1.0 The `$args` parameter was added. * * @param int $term_id Term ID. * @param string $taxonomy Taxonomy slug. * @param array $args Arguments passed to wp_update_term(). */ do_action( 'edited_terms', $term_id, $taxonomy, $args ); /** * Fires immediate before a term-taxonomy relationship is updated. * * @since 2.9.0 * @since 6.1.0 The `$args` parameter was added. * * @param int $tt_id Term taxonomy ID. * @param string $taxonomy Taxonomy slug. * @param array $args Arguments passed to wp_update_term(). */ do_action( 'edit_term_taxonomy', $tt_id, $taxonomy, $args ); $wpdb->update( $wpdb->term_taxonomy, compact( 'term_id', 'taxonomy', 'description', 'parent' ), array( 'term_taxonomy_id' => $tt_id ) ); /** * Fires immediately after a term-taxonomy relationship is updated. * * @since 2.9.0 * @since 6.1.0 The `$args` parameter was added. * * @param int $tt_id Term taxonomy ID. * @param string $taxonomy Taxonomy slug. * @param array $args Arguments passed to wp_update_term(). */ do_action( 'edited_term_taxonomy', $tt_id, $taxonomy, $args ); /** * Fires after a term has been updated, but before the term cache has been cleaned. * * The {@see 'edit_$taxonomy'} hook is also available for targeting a specific * taxonomy. * * @since 2.3.0 * @since 6.1.0 The `$args` parameter was added. * * @param int $term_id Term ID. * @param int $tt_id Term taxonomy ID. * @param string $taxonomy Taxonomy slug. * @param array $args Arguments passed to wp_update_term(). */ do_action( 'edit_term', $term_id, $tt_id, $taxonomy, $args ); /** * Fires after a term in a specific taxonomy has been updated, but before the term * cache has been cleaned. * * The dynamic portion of the hook name, `$taxonomy`, refers to the taxonomy slug. * * Possible hook names include: * * - `edit_category` * - `edit_post_tag` * * @since 2.3.0 * @since 6.1.0 The `$args` parameter was added. * * @param int $term_id Term ID. * @param int $tt_id Term taxonomy ID. * @param array $args Arguments passed to wp_update_term(). */ do_action( "edit_{$taxonomy}", $term_id, $tt_id, $args ); /** This filter is documented in wp-includes/taxonomy.php */ $term_id = apply_filters( 'term_id_filter', $term_id, $tt_id ); clean_term_cache( $term_id, $taxonomy ); /** * Fires after a term has been updated, and the term cache has been cleaned. * * The {@see 'edited_$taxonomy'} hook is also available for targeting a specific * taxonomy. * * @since 2.3.0 * @since 6.1.0 The `$args` parameter was added. * * @param int $term_id Term ID. * @param int $tt_id Term taxonomy ID. * @param string $taxonomy Taxonomy slug. * @param array $args Arguments passed to wp_update_term(). */ do_action( 'edited_term', $term_id, $tt_id, $taxonomy, $args ); /** * Fires after a term for a specific taxonomy has been updated, and the term * cache has been cleaned. * * The dynamic portion of the hook name, `$taxonomy`, refers to the taxonomy slug. * * Possible hook names include: * * - `edited_category` * - `edited_post_tag` * * @since 2.3.0 * @since 6.1.0 The `$args` parameter was added. * * @param int $term_id Term ID. * @param int $tt_id Term taxonomy ID. * @param array $args Arguments passed to wp_update_term(). */ do_action( "edited_{$taxonomy}", $term_id, $tt_id, $args ); /** This action is documented in wp-includes/taxonomy.php */ do_action( 'saved_term', $term_id, $tt_id, $taxonomy, true, $args ); /** This action is documented in wp-includes/taxonomy.php */ do_action( "saved_{$taxonomy}", $term_id, $tt_id, true, $args ); return array( 'term_id' => $term_id, 'term_taxonomy_id' => $tt_id, ); } /** * Enables or disables term counting. * * @since 2.5.0 * * @param bool $defer Optional. Enable if true, disable if false. * @return bool Whether term counting is enabled or disabled. */ function wp_defer_term_counting( $defer = null ) { static $_defer = false; if ( is_bool( $defer ) ) { $_defer = $defer; // Flush any deferred counts. if ( ! $defer ) { wp_update_term_count( null, null, true ); } } return $_defer; } /** * Updates the amount of terms in taxonomy. * * If there is a taxonomy callback applied, then it will be called for updating * the count. * * The default action is to count what the amount of terms have the relationship * of term ID. Once that is done, then update the database. * * @since 2.3.0 * * @param int|array $terms The term_taxonomy_id of the terms. * @param string $taxonomy The context of the term. * @param bool $do_deferred Whether to flush the deferred term counts too. Default false. * @return bool If no terms will return false, and if successful will return true. */ function wp_update_term_count( $terms, $taxonomy, $do_deferred = false ) { static $_deferred = array(); if ( $do_deferred ) { foreach ( (array) array_keys( $_deferred ) as $tax ) { wp_update_term_count_now( $_deferred[ $tax ], $tax ); unset( $_deferred[ $tax ] ); } } if ( empty( $terms ) ) { return false; } if ( ! is_array( $terms ) ) { $terms = array( $terms ); } if ( wp_defer_term_counting() ) { if ( ! isset( $_deferred[ $taxonomy ] ) ) { $_deferred[ $taxonomy ] = array(); } $_deferred[ $taxonomy ] = array_unique( array_merge( $_deferred[ $taxonomy ], $terms ) ); return true; } return wp_update_term_count_now( $terms, $taxonomy ); } /** * Performs term count update immediately. * * @since 2.5.0 * * @param array $terms The term_taxonomy_id of terms to update. * @param string $taxonomy The context of the term. * @return true Always true when complete. */ function wp_update_term_count_now( $terms, $taxonomy ) { $terms = array_map( 'intval', $terms ); $taxonomy = get_taxonomy( $taxonomy ); if ( ! empty( $taxonomy->update_count_callback ) ) { call_user_func( $taxonomy->update_count_callback, $terms, $taxonomy ); } else { $object_types = (array) $taxonomy->object_type; foreach ( $object_types as &$object_type ) { if ( str_starts_with( $object_type, 'attachment:' ) ) { list( $object_type ) = explode( ':', $object_type ); } } if ( array_filter( $object_types, 'post_type_exists' ) == $object_types ) { // Only post types are attached to this taxonomy. _update_post_term_count( $terms, $taxonomy ); } else { // Default count updater. _update_generic_term_count( $terms, $taxonomy ); } } clean_term_cache( $terms, '', false ); return true; } // // Cache. // /** * Removes the taxonomy relationship to terms from the cache. * * Will remove the entire taxonomy relationship containing term `$object_id`. The * term IDs have to exist within the taxonomy `$object_type` for the deletion to * take place. * * @since 2.3.0 * * @global bool $_wp_suspend_cache_invalidation * * @see get_object_taxonomies() for more on $object_type. * * @param int|array $object_ids Single or list of term object ID(s). * @param array|string $object_type The taxonomy object type. */ function clean_object_term_cache( $object_ids, $object_type ) { global $_wp_suspend_cache_invalidation; if ( ! empty( $_wp_suspend_cache_invalidation ) ) { return; } if ( ! is_array( $object_ids ) ) { $object_ids = array( $object_ids ); } $taxonomies = get_object_taxonomies( $object_type ); foreach ( $taxonomies as $taxonomy ) { wp_cache_delete_multiple( $object_ids, "{$taxonomy}_relationships" ); } wp_cache_set_terms_last_changed(); /** * Fires after the object term cache has been cleaned. * * @since 2.5.0 * * @param array $object_ids An array of object IDs. * @param string $object_type Object type. */ do_action( 'clean_object_term_cache', $object_ids, $object_type ); } /** * Removes all of the term IDs from the cache. * * @since 2.3.0 * * @global wpdb $wpdb WordPress database abstraction object. * @global bool $_wp_suspend_cache_invalidation * * @param int|int[] $ids Single or array of term IDs. * @param string $taxonomy Optional. Taxonomy slug. Can be empty, in which case the taxonomies of the passed * term IDs will be used. Default empty. * @param bool $clean_taxonomy Optional. Whether to clean taxonomy wide caches (true), or just individual * term object caches (false). Default true. */ function clean_term_cache( $ids, $taxonomy = '', $clean_taxonomy = true ) { global $wpdb, $_wp_suspend_cache_invalidation; if ( ! empty( $_wp_suspend_cache_invalidation ) ) { return; } if ( ! is_array( $ids ) ) { $ids = array( $ids ); } $taxonomies = array(); // If no taxonomy, assume tt_ids. if ( empty( $taxonomy ) ) { $tt_ids = array_map( 'intval', $ids ); $tt_ids = implode( ', ', $tt_ids ); $terms = $wpdb->get_results( "SELECT term_id, taxonomy FROM $wpdb->term_taxonomy WHERE term_taxonomy_id IN ($tt_ids)" ); $ids = array(); foreach ( (array) $terms as $term ) { $taxonomies[] = $term->taxonomy; $ids[] = $term->term_id; } wp_cache_delete_multiple( $ids, 'terms' ); $taxonomies = array_unique( $taxonomies ); } else { wp_cache_delete_multiple( $ids, 'terms' ); $taxonomies = array( $taxonomy ); } foreach ( $taxonomies as $taxonomy ) { if ( $clean_taxonomy ) { clean_taxonomy_cache( $taxonomy ); } /** * Fires once after each taxonomy's term cache has been cleaned. * * @since 2.5.0 * @since 4.5.0 Added the `$clean_taxonomy` parameter. * * @param array $ids An array of term IDs. * @param string $taxonomy Taxonomy slug. * @param bool $clean_taxonomy Whether or not to clean taxonomy-wide caches */ do_action( 'clean_term_cache', $ids, $taxonomy, $clean_taxonomy ); } wp_cache_set_terms_last_changed(); } /** * Cleans the caches for a taxonomy. * * @since 4.9.0 * * @param string $taxonomy Taxonomy slug. */ function clean_taxonomy_cache( $taxonomy ) { wp_cache_delete( 'all_ids', $taxonomy ); wp_cache_delete( 'get', $taxonomy ); wp_cache_set_terms_last_changed(); // Regenerate cached hierarchy. delete_option( "{$taxonomy}_children" ); _get_term_hierarchy( $taxonomy ); /** * Fires after a taxonomy's caches have been cleaned. * * @since 4.9.0 * * @param string $taxonomy Taxonomy slug. */ do_action( 'clean_taxonomy_cache', $taxonomy ); } /** * Retrieves the cached term objects for the given object ID. * * Upstream functions (like get_the_terms() and is_object_in_term()) are * responsible for populating the object-term relationship cache. The current * function only fetches relationship data that is already in the cache. * * @since 2.3.0 * @since 4.7.0 Returns a `WP_Error` object if there's an error with * any of the matched terms. * * @param int $id Term object ID, for example a post, comment, or user ID. * @param string $taxonomy Taxonomy name. * @return bool|WP_Term[]|WP_Error Array of `WP_Term` objects, if cached. * False if cache is empty for `$taxonomy` and `$id`. * WP_Error if get_term() returns an error object for any term. */ function get_object_term_cache( $id, $taxonomy ) { $_term_ids = wp_cache_get( $id, "{$taxonomy}_relationships" ); // We leave the priming of relationship caches to upstream functions. if ( false === $_term_ids ) { return false; } // Backward compatibility for if a plugin is putting objects into the cache, rather than IDs. $term_ids = array(); foreach ( $_term_ids as $term_id ) { if ( is_numeric( $term_id ) ) { $term_ids[] = (int) $term_id; } elseif ( isset( $term_id->term_id ) ) { $term_ids[] = (int) $term_id->term_id; } } // Fill the term objects. _prime_term_caches( $term_ids ); $terms = array(); foreach ( $term_ids as $term_id ) { $term = get_term( $term_id, $taxonomy ); if ( is_wp_error( $term ) ) { return $term; } $terms[] = $term; } return $terms; } /** * Updates the cache for the given term object ID(s). * * Note: Due to performance concerns, great care should be taken to only update * term caches when necessary. Processing time can increase exponentially depending * on both the number of passed term IDs and the number of taxonomies those terms * belong to. * * Caches will only be updated for terms not already cached. * * @since 2.3.0 * * @param string|int[] $object_ids Comma-separated list or array of term object IDs. * @param string|string[] $object_type The taxonomy object type or array of the same. * @return void|false Void on success or if the `$object_ids` parameter is empty, * false if all of the terms in `$object_ids` are already cached. */ function update_object_term_cache( $object_ids, $object_type ) { if ( empty( $object_ids ) ) { return; } if ( ! is_array( $object_ids ) ) { $object_ids = explode( ',', $object_ids ); } $object_ids = array_map( 'intval', $object_ids ); $non_cached_ids = array(); $taxonomies = get_object_taxonomies( $object_type ); foreach ( $taxonomies as $taxonomy ) { $cache_values = wp_cache_get_multiple( (array) $object_ids, "{$taxonomy}_relationships" ); foreach ( $cache_values as $id => $value ) { if ( false === $value ) { $non_cached_ids[] = $id; } } } if ( empty( $non_cached_ids ) ) { return false; } $non_cached_ids = array_unique( $non_cached_ids ); $terms = wp_get_object_terms( $non_cached_ids, $taxonomies, array( 'fields' => 'all_with_object_id', 'orderby' => 'name', 'update_term_meta_cache' => false, ) ); $object_terms = array(); foreach ( (array) $terms as $term ) { $object_terms[ $term->object_id ][ $term->taxonomy ][] = $term->term_id; } foreach ( $non_cached_ids as $id ) { foreach ( $taxonomies as $taxonomy ) { if ( ! isset( $object_terms[ $id ][ $taxonomy ] ) ) { if ( ! isset( $object_terms[ $id ] ) ) { $object_terms[ $id ] = array(); } $object_terms[ $id ][ $taxonomy ] = array(); } } } $cache_values = array(); foreach ( $object_terms as $id => $value ) { foreach ( $value as $taxonomy => $terms ) { $cache_values[ $taxonomy ][ $id ] = $terms; } } foreach ( $cache_values as $taxonomy => $data ) { wp_cache_add_multiple( $data, "{$taxonomy}_relationships" ); } } /** * Updates terms in cache. * * @since 2.3.0 * * @param WP_Term[] $terms Array of term objects to change. * @param string $taxonomy Not used. */ function update_term_cache( $terms, $taxonomy = '' ) { $data = array(); foreach ( (array) $terms as $term ) { // Create a copy in case the array was passed by reference. $_term = clone $term; // Object ID should not be cached. unset( $_term->object_id ); $data[ $term->term_id ] = $_term; } wp_cache_add_multiple( $data, 'terms' ); } // // Private. // /** * Retrieves children of taxonomy as term IDs. * * @access private * @since 2.3.0 * * @param string $taxonomy Taxonomy name. * @return array Empty if $taxonomy isn't hierarchical or returns children as term IDs. */ function _get_term_hierarchy( $taxonomy ) { if ( ! is_taxonomy_hierarchical( $taxonomy ) ) { return array(); } $children = get_option( "{$taxonomy}_children" ); if ( is_array( $children ) ) { return $children; } $children = array(); $terms = get_terms( array( 'taxonomy' => $taxonomy, 'get' => 'all', 'orderby' => 'id', 'fields' => 'id=>parent', 'update_term_meta_cache' => false, ) ); foreach ( $terms as $term_id => $parent ) { if ( $parent > 0 ) { $children[ $parent ][] = $term_id; } } update_option( "{$taxonomy}_children", $children ); return $children; } /** * Gets the subset of $terms that are descendants of $term_id. * * If `$terms` is an array of objects, then _get_term_children() returns an array of objects. * If `$terms` is an array of IDs, then _get_term_children() returns an array of IDs. * * @access private * @since 2.3.0 * * @param int $term_id The ancestor term: all returned terms should be descendants of `$term_id`. * @param array $terms The set of terms - either an array of term objects or term IDs - from which those that * are descendants of $term_id will be chosen. * @param string $taxonomy The taxonomy which determines the hierarchy of the terms. * @param array $ancestors Optional. Term ancestors that have already been identified. Passed by reference, to keep * track of found terms when recursing the hierarchy. The array of located ancestors is used * to prevent infinite recursion loops. For performance, `term_ids` are used as array keys, * with 1 as value. Default empty array. * @return array|WP_Error The subset of $terms that are descendants of $term_id. */ function _get_term_children( $term_id, $terms, $taxonomy, &$ancestors = array() ) { $empty_array = array(); if ( empty( $terms ) ) { return $empty_array; } $term_id = (int) $term_id; $term_list = array(); $has_children = _get_term_hierarchy( $taxonomy ); if ( $term_id && ! isset( $has_children[ $term_id ] ) ) { return $empty_array; } // Include the term itself in the ancestors array, so we can properly detect when a loop has occurred. if ( empty( $ancestors ) ) { $ancestors[ $term_id ] = 1; } foreach ( (array) $terms as $term ) { $use_id = false; if ( ! is_object( $term ) ) { $term = get_term( $term, $taxonomy ); if ( is_wp_error( $term ) ) { return $term; } $use_id = true; } // Don't recurse if we've already identified the term as a child - this indicates a loop. if ( isset( $ancestors[ $term->term_id ] ) ) { continue; } if ( (int) $term->parent === $term_id ) { if ( $use_id ) { $term_list[] = $term->term_id; } else { $term_list[] = $term; } if ( ! isset( $has_children[ $term->term_id ] ) ) { continue; } $ancestors[ $term->term_id ] = 1; $children = _get_term_children( $term->term_id, $terms, $taxonomy, $ancestors ); if ( $children ) { $term_list = array_merge( $term_list, $children ); } } } return $term_list; } /** * Adds count of children to parent count. * * Recalculates term counts by including items from child terms. Assumes all * relevant children are already in the $terms argument. * * @access private * @since 2.3.0 * * @global wpdb $wpdb WordPress database abstraction object. * * @param object[]|WP_Term[] $terms List of term objects (passed by reference). * @param string $taxonomy Term context. */ function _pad_term_counts( &$terms, $taxonomy ) { global $wpdb; // This function only works for hierarchical taxonomies like post categories. if ( ! is_taxonomy_hierarchical( $taxonomy ) ) { return; } $term_hier = _get_term_hierarchy( $taxonomy ); if ( empty( $term_hier ) ) { return; } $term_items = array(); $terms_by_id = array(); $term_ids = array(); foreach ( (array) $terms as $key => $term ) { $terms_by_id[ $term->term_id ] = & $terms[ $key ]; $term_ids[ $term->term_taxonomy_id ] = $term->term_id; } // Get the object and term IDs and stick them in a lookup table. $tax_obj = get_taxonomy( $taxonomy ); $object_types = esc_sql( $tax_obj->object_type ); $results = $wpdb->get_results( "SELECT object_id, term_taxonomy_id FROM $wpdb->term_relationships INNER JOIN $wpdb->posts ON object_id = ID WHERE term_taxonomy_id IN (" . implode( ',', array_keys( $term_ids ) ) . ") AND post_type IN ('" . implode( "', '", $object_types ) . "') AND post_status = 'publish'" ); foreach ( $results as $row ) { $id = $term_ids[ $row->term_taxonomy_id ]; $term_items[ $id ][ $row->object_id ] = isset( $term_items[ $id ][ $row->object_id ] ) ? ++$term_items[ $id ][ $row->object_id ] : 1; } // Touch every ancestor's lookup row for each post in each term. foreach ( $term_ids as $term_id ) { $child = $term_id; $ancestors = array(); while ( ! empty( $terms_by_id[ $child ] ) && $parent = $terms_by_id[ $child ]->parent ) { $ancestors[] = $child; if ( ! empty( $term_items[ $term_id ] ) ) { foreach ( $term_items[ $term_id ] as $item_id => $touches ) { $term_items[ $parent ][ $item_id ] = isset( $term_items[ $parent ][ $item_id ] ) ? ++$term_items[ $parent ][ $item_id ] : 1; } } $child = $parent; if ( in_array( $parent, $ancestors, true ) ) { break; } } } // Transfer the touched cells. foreach ( (array) $term_items as $id => $items ) { if ( isset( $terms_by_id[ $id ] ) ) { $terms_by_id[ $id ]->count = count( $items ); } } } /** * Adds any terms from the given IDs to the cache that do not already exist in cache. * * @since 4.6.0 * @since 6.1.0 This function is no longer marked as "private". * @since 6.3.0 Use wp_lazyload_term_meta() for lazy-loading of term meta. * * @global wpdb $wpdb WordPress database abstraction object. * * @param array $term_ids Array of term IDs. * @param bool $update_meta_cache Optional. Whether to update the meta cache. Default true. */ function _prime_term_caches( $term_ids, $update_meta_cache = true ) { global $wpdb; $non_cached_ids = _get_non_cached_ids( $term_ids, 'terms' ); if ( ! empty( $non_cached_ids ) ) { $fresh_terms = $wpdb->get_results( sprintf( "SELECT t.*, tt.* FROM $wpdb->terms AS t INNER JOIN $wpdb->term_taxonomy AS tt ON t.term_id = tt.term_id WHERE t.term_id IN (%s)", implode( ',', array_map( 'intval', $non_cached_ids ) ) ) ); update_term_cache( $fresh_terms ); } if ( $update_meta_cache ) { wp_lazyload_term_meta( $term_ids ); } } // // Default callbacks. // /** * Updates term count based on object types of the current taxonomy. * * Private function for the default callback for post_tag and category * taxonomies. * * @access private * @since 2.3.0 * * @global wpdb $wpdb WordPress database abstraction object. * * @param int[] $terms List of term taxonomy IDs. * @param WP_Taxonomy $taxonomy Current taxonomy object of terms. */ function _update_post_term_count( $terms, $taxonomy ) { global $wpdb; $object_types = (array) $taxonomy->object_type; foreach ( $object_types as &$object_type ) { list( $object_type ) = explode( ':', $object_type ); } $object_types = array_unique( $object_types ); $check_attachments = array_search( 'attachment', $object_types, true ); if ( false !== $check_attachments ) { unset( $object_types[ $check_attachments ] ); $check_attachments = true; } if ( $object_types ) { $object_types = esc_sql( array_filter( $object_types, 'post_type_exists' ) ); } $post_statuses = array( 'publish' ); /** * Filters the post statuses for updating the term count. * * @since 5.7.0 * * @param string[] $post_statuses List of post statuses to include in the count. Default is 'publish'. * @param WP_Taxonomy $taxonomy Current taxonomy object. */ $post_statuses = esc_sql( apply_filters( 'update_post_term_count_statuses', $post_statuses, $taxonomy ) ); foreach ( (array) $terms as $term ) { $count = 0; // Attachments can be 'inherit' status, we need to base count off the parent's status if so. if ( $check_attachments ) { // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.QuotedDynamicPlaceholderGeneration $count += (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM $wpdb->term_relationships, $wpdb->posts p1 WHERE p1.ID = $wpdb->term_relationships.object_id AND ( post_status IN ('" . implode( "', '", $post_statuses ) . "') OR ( post_status = 'inherit' AND post_parent > 0 AND ( SELECT post_status FROM $wpdb->posts WHERE ID = p1.post_parent ) IN ('" . implode( "', '", $post_statuses ) . "') ) ) AND post_type = 'attachment' AND term_taxonomy_id = %d", $term ) ); } if ( $object_types ) { // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.QuotedDynamicPlaceholderGeneration $count += (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM $wpdb->term_relationships, $wpdb->posts WHERE $wpdb->posts.ID = $wpdb->term_relationships.object_id AND post_status IN ('" . implode( "', '", $post_statuses ) . "') AND post_type IN ('" . implode( "', '", $object_types ) . "') AND term_taxonomy_id = %d", $term ) ); } /** This action is documented in wp-includes/taxonomy.php */ do_action( 'edit_term_taxonomy', $term, $taxonomy->name ); $wpdb->update( $wpdb->term_taxonomy, compact( 'count' ), array( 'term_taxonomy_id' => $term ) ); /** This action is documented in wp-includes/taxonomy.php */ do_action( 'edited_term_taxonomy', $term, $taxonomy->name ); } } /** * Updates term count based on number of objects. * * Default callback for the 'link_category' taxonomy. * * @since 3.3.0 * * @global wpdb $wpdb WordPress database abstraction object. * * @param int[] $terms List of term taxonomy IDs. * @param WP_Taxonomy $taxonomy Current taxonomy object of terms. */ function _update_generic_term_count( $terms, $taxonomy ) { global $wpdb; foreach ( (array) $terms as $term ) { $count = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM $wpdb->term_relationships WHERE term_taxonomy_id = %d", $term ) ); /** This action is documented in wp-includes/taxonomy.php */ do_action( 'edit_term_taxonomy', $term, $taxonomy->name ); $wpdb->update( $wpdb->term_taxonomy, compact( 'count' ), array( 'term_taxonomy_id' => $term ) ); /** This action is documented in wp-includes/taxonomy.php */ do_action( 'edited_term_taxonomy', $term, $taxonomy->name ); } } /** * Creates a new term for a term_taxonomy item that currently shares its term * with another term_taxonomy. * * @ignore * @since 4.2.0 * @since 4.3.0 Introduced `$record` parameter. Also, `$term_id` and * `$term_taxonomy_id` can now accept objects. * * @global wpdb $wpdb WordPress database abstraction object. * * @param int|object $term_id ID of the shared term, or the shared term object. * @param int|object $term_taxonomy_id ID of the term_taxonomy item to receive a new term, or the term_taxonomy object * (corresponding to a row from the term_taxonomy table). * @param bool $record Whether to record data about the split term in the options table. The recording * process has the potential to be resource-intensive, so during batch operations * it can be beneficial to skip inline recording and do it just once, after the * batch is processed. Only set this to `false` if you know what you are doing. * Default: true. * @return int|WP_Error When the current term does not need to be split (or cannot be split on the current * database schema), `$term_id` is returned. When the term is successfully split, the * new term_id is returned. A WP_Error is returned for miscellaneous errors. */ function _split_shared_term( $term_id, $term_taxonomy_id, $record = true ) { global $wpdb; if ( is_object( $term_id ) ) { $shared_term = $term_id; $term_id = (int) $shared_term->term_id; } if ( is_object( $term_taxonomy_id ) ) { $term_taxonomy = $term_taxonomy_id; $term_taxonomy_id = (int) $term_taxonomy->term_taxonomy_id; } // If there are no shared term_taxonomy rows, there's nothing to do here. $shared_tt_count = (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM $wpdb->term_taxonomy tt WHERE tt.term_id = %d AND tt.term_taxonomy_id != %d", $term_id, $term_taxonomy_id ) ); if ( ! $shared_tt_count ) { return $term_id; } /* * Verify that the term_taxonomy_id passed to the function is actually associated with the term_id. * If there's a mismatch, it may mean that the term is already split. Return the actual term_id from the db. */ $check_term_id = (int) $wpdb->get_var( $wpdb->prepare( "SELECT term_id FROM $wpdb->term_taxonomy WHERE term_taxonomy_id = %d", $term_taxonomy_id ) ); if ( $check_term_id !== $term_id ) { return $check_term_id; } // Pull up data about the currently shared slug, which we'll use to populate the new one. if ( empty( $shared_term ) ) { $shared_term = $wpdb->get_row( $wpdb->prepare( "SELECT t.* FROM $wpdb->terms t WHERE t.term_id = %d", $term_id ) ); } $new_term_data = array( 'name' => $shared_term->name, 'slug' => $shared_term->slug, 'term_group' => $shared_term->term_group, ); if ( false === $wpdb->insert( $wpdb->terms, $new_term_data ) ) { return new WP_Error( 'db_insert_error', __( 'Could not split shared term.' ), $wpdb->last_error ); } $new_term_id = (int) $wpdb->insert_id; // Update the existing term_taxonomy to point to the newly created term. $wpdb->update( $wpdb->term_taxonomy, array( 'term_id' => $new_term_id ), array( 'term_taxonomy_id' => $term_taxonomy_id ) ); // Reassign child terms to the new parent. if ( empty( $term_taxonomy ) ) { $term_taxonomy = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM $wpdb->term_taxonomy WHERE term_taxonomy_id = %d", $term_taxonomy_id ) ); } $children_tt_ids = $wpdb->get_col( $wpdb->prepare( "SELECT term_taxonomy_id FROM $wpdb->term_taxonomy WHERE parent = %d AND taxonomy = %s", $term_id, $term_taxonomy->taxonomy ) ); if ( ! empty( $children_tt_ids ) ) { foreach ( $children_tt_ids as $child_tt_id ) { $wpdb->update( $wpdb->term_taxonomy, array( 'parent' => $new_term_id ), array( 'term_taxonomy_id' => $child_tt_id ) ); clean_term_cache( (int) $child_tt_id, '', false ); } } else { // If the term has no children, we must force its taxonomy cache to be rebuilt separately. clean_term_cache( $new_term_id, $term_taxonomy->taxonomy, false ); } clean_term_cache( $term_id, $term_taxonomy->taxonomy, false ); /* * Taxonomy cache clearing is delayed to avoid race conditions that may occur when * regenerating the taxonomy's hierarchy tree. */ $taxonomies_to_clean = array( $term_taxonomy->taxonomy ); // Clean the cache for term taxonomies formerly shared with the current term. $shared_term_taxonomies = $wpdb->get_col( $wpdb->prepare( "SELECT taxonomy FROM $wpdb->term_taxonomy WHERE term_id = %d", $term_id ) ); $taxonomies_to_clean = array_merge( $taxonomies_to_clean, $shared_term_taxonomies ); foreach ( $taxonomies_to_clean as $taxonomy_to_clean ) { clean_taxonomy_cache( $taxonomy_to_clean ); } // Keep a record of term_ids that have been split, keyed by old term_id. See wp_get_split_term(). if ( $record ) { $split_term_data = get_option( '_split_terms', array() ); if ( ! isset( $split_term_data[ $term_id ] ) ) { $split_term_data[ $term_id ] = array(); } $split_term_data[ $term_id ][ $term_taxonomy->taxonomy ] = $new_term_id; update_option( '_split_terms', $split_term_data ); } // If we've just split the final shared term, set the "finished" flag. $shared_terms_exist = $wpdb->get_results( "SELECT tt.term_id, t.*, count(*) as term_tt_count FROM {$wpdb->term_taxonomy} tt LEFT JOIN {$wpdb->terms} t ON t.term_id = tt.term_id GROUP BY t.term_id HAVING term_tt_count > 1 LIMIT 1" ); if ( ! $shared_terms_exist ) { update_option( 'finished_splitting_shared_terms', true ); } /** * Fires after a previously shared taxonomy term is split into two separate terms. * * @since 4.2.0 * * @param int $term_id ID of the formerly shared term. * @param int $new_term_id ID of the new term created for the $term_taxonomy_id. * @param int $term_taxonomy_id ID for the term_taxonomy row affected by the split. * @param string $taxonomy Taxonomy for the split term. */ do_action( 'split_shared_term', $term_id, $new_term_id, $term_taxonomy_id, $term_taxonomy->taxonomy ); return $new_term_id; } /** * Splits a batch of shared taxonomy terms. * * @since 4.3.0 * * @global wpdb $wpdb WordPress database abstraction object. */ function _wp_batch_split_terms() { global $wpdb; $lock_name = 'term_split.lock'; // Try to lock. $lock_result = $wpdb->query( $wpdb->prepare( "INSERT IGNORE INTO `$wpdb->options` ( `option_name`, `option_value`, `autoload` ) VALUES (%s, %s, 'no') /* LOCK */", $lock_name, time() ) ); if ( ! $lock_result ) { $lock_result = get_option( $lock_name ); // Bail if we were unable to create a lock, or if the existing lock is still valid. if ( ! $lock_result || ( $lock_result > ( time() - HOUR_IN_SECONDS ) ) ) { wp_schedule_single_event( time() + ( 5 * MINUTE_IN_SECONDS ), 'wp_split_shared_term_batch' ); return; } } // Update the lock, as by this point we've definitely got a lock, just need to fire the actions. update_option( $lock_name, time() ); // Get a list of shared terms (those with more than one associated row in term_taxonomy). $shared_terms = $wpdb->get_results( "SELECT tt.term_id, t.*, count(*) as term_tt_count FROM {$wpdb->term_taxonomy} tt LEFT JOIN {$wpdb->terms} t ON t.term_id = tt.term_id GROUP BY t.term_id HAVING term_tt_count > 1 LIMIT 10" ); // No more terms, we're done here. if ( ! $shared_terms ) { update_option( 'finished_splitting_shared_terms', true ); delete_option( $lock_name ); return; } // Shared terms found? We'll need to run this script again. wp_schedule_single_event( time() + ( 2 * MINUTE_IN_SECONDS ), 'wp_split_shared_term_batch' ); // Rekey shared term array for faster lookups. $_shared_terms = array(); foreach ( $shared_terms as $shared_term ) { $term_id = (int) $shared_term->term_id; $_shared_terms[ $term_id ] = $shared_term; } $shared_terms = $_shared_terms; // Get term taxonomy data for all shared terms. $shared_term_ids = implode( ',', array_keys( $shared_terms ) ); $shared_tts = $wpdb->get_results( "SELECT * FROM {$wpdb->term_taxonomy} WHERE `term_id` IN ({$shared_term_ids})" ); // Split term data recording is slow, so we do it just once, outside the loop. $split_term_data = get_option( '_split_terms', array() ); $skipped_first_term = array(); $taxonomies = array(); foreach ( $shared_tts as $shared_tt ) { $term_id = (int) $shared_tt->term_id; // Don't split the first tt belonging to a given term_id. if ( ! isset( $skipped_first_term[ $term_id ] ) ) { $skipped_first_term[ $term_id ] = 1; continue; } if ( ! isset( $split_term_data[ $term_id ] ) ) { $split_term_data[ $term_id ] = array(); } // Keep track of taxonomies whose hierarchies need flushing. if ( ! isset( $taxonomies[ $shared_tt->taxonomy ] ) ) { $taxonomies[ $shared_tt->taxonomy ] = 1; } // Split the term. $split_term_data[ $term_id ][ $shared_tt->taxonomy ] = _split_shared_term( $shared_terms[ $term_id ], $shared_tt, false ); } // Rebuild the cached hierarchy for each affected taxonomy. foreach ( array_keys( $taxonomies ) as $tax ) { delete_option( "{$tax}_children" ); _get_term_hierarchy( $tax ); } update_option( '_split_terms', $split_term_data ); delete_option( $lock_name ); } /** * In order to avoid the _wp_batch_split_terms() job being accidentally removed, * checks that it's still scheduled while we haven't finished splitting terms. * * @ignore * @since 4.3.0 */ function _wp_check_for_scheduled_split_terms() { if ( ! get_option( 'finished_splitting_shared_terms' ) && ! wp_next_scheduled( 'wp_split_shared_term_batch' ) ) { wp_schedule_single_event( time() + MINUTE_IN_SECONDS, 'wp_split_shared_term_batch' ); } } /** * Checks default categories when a term gets split to see if any of them need to be updated. * * @ignore * @since 4.2.0 * * @param int $term_id ID of the formerly shared term. * @param int $new_term_id ID of the new term created for the $term_taxonomy_id. * @param int $term_taxonomy_id ID for the term_taxonomy row affected by the split. * @param string $taxonomy Taxonomy for the split term. */ function _wp_check_split_default_terms( $term_id, $new_term_id, $term_taxonomy_id, $taxonomy ) { if ( 'category' !== $taxonomy ) { return; } foreach ( array( 'default_category', 'default_link_category', 'default_email_category' ) as $option ) { if ( (int) get_option( $option, -1 ) === $term_id ) { update_option( $option, $new_term_id ); } } } /** * Checks menu items when a term gets split to see if any of them need to be updated. * * @ignore * @since 4.2.0 * * @global wpdb $wpdb WordPress database abstraction object. * * @param int $term_id ID of the formerly shared term. * @param int $new_term_id ID of the new term created for the $term_taxonomy_id. * @param int $term_taxonomy_id ID for the term_taxonomy row affected by the split. * @param string $taxonomy Taxonomy for the split term. */ function _wp_check_split_terms_in_menus( $term_id, $new_term_id, $term_taxonomy_id, $taxonomy ) { global $wpdb; $post_ids = $wpdb->get_col( $wpdb->prepare( "SELECT m1.post_id FROM {$wpdb->postmeta} AS m1 INNER JOIN {$wpdb->postmeta} AS m2 ON ( m2.post_id = m1.post_id ) INNER JOIN {$wpdb->postmeta} AS m3 ON ( m3.post_id = m1.post_id ) WHERE ( m1.meta_key = '_menu_item_type' AND m1.meta_value = 'taxonomy' ) AND ( m2.meta_key = '_menu_item_object' AND m2.meta_value = %s ) AND ( m3.meta_key = '_menu_item_object_id' AND m3.meta_value = %d )", $taxonomy, $term_id ) ); if ( $post_ids ) { foreach ( $post_ids as $post_id ) { update_post_meta( $post_id, '_menu_item_object_id', $new_term_id, $term_id ); } } } /** * If the term being split is a nav_menu, changes associations. * * @ignore * @since 4.3.0 * * @param int $term_id ID of the formerly shared term. * @param int $new_term_id ID of the new term created for the $term_taxonomy_id. * @param int $term_taxonomy_id ID for the term_taxonomy row affected by the split. * @param string $taxonomy Taxonomy for the split term. */ function _wp_check_split_nav_menu_terms( $term_id, $new_term_id, $term_taxonomy_id, $taxonomy ) { if ( 'nav_menu' !== $taxonomy ) { return; } // Update menu locations. $locations = get_nav_menu_locations(); foreach ( $locations as $location => $menu_id ) { if ( $term_id === $menu_id ) { $locations[ $location ] = $new_term_id; } } set_theme_mod( 'nav_menu_locations', $locations ); } /** * Gets data about terms that previously shared a single term_id, but have since been split. * * @since 4.2.0 * * @param int $old_term_id Term ID. This is the old, pre-split term ID. * @return array Array of new term IDs, keyed by taxonomy. */ function wp_get_split_terms( $old_term_id ) { $split_terms = get_option( '_split_terms', array() ); $terms = array(); if ( isset( $split_terms[ $old_term_id ] ) ) { $terms = $split_terms[ $old_term_id ]; } return $terms; } /** * Gets the new term ID corresponding to a previously split term. * * @since 4.2.0 * * @param int $old_term_id Term ID. This is the old, pre-split term ID. * @param string $taxonomy Taxonomy that the term belongs to. * @return int|false If a previously split term is found corresponding to the old term_id and taxonomy, * the new term_id will be returned. If no previously split term is found matching * the parameters, returns false. */ function wp_get_split_term( $old_term_id, $taxonomy ) { $split_terms = wp_get_split_terms( $old_term_id ); $term_id = false; if ( isset( $split_terms[ $taxonomy ] ) ) { $term_id = (int) $split_terms[ $taxonomy ]; } return $term_id; } /** * Determines whether a term is shared between multiple taxonomies. * * Shared taxonomy terms began to be split in 4.3, but failed cron tasks or * other delays in upgrade routines may cause shared terms to remain. * * @since 4.4.0 * * @global wpdb $wpdb WordPress database abstraction object. * * @param int $term_id Term ID. * @return bool Returns false if a term is not shared between multiple taxonomies or * if splitting shared taxonomy terms is finished. */ function wp_term_is_shared( $term_id ) { global $wpdb; if ( get_option( 'finished_splitting_shared_terms' ) ) { return false; } $tt_count = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM $wpdb->term_taxonomy WHERE term_id = %d", $term_id ) ); return $tt_count > 1; } /** * Generates a permalink for a taxonomy term archive. * * @since 2.5.0 * * @global WP_Rewrite $wp_rewrite WordPress rewrite component. * * @param WP_Term|int|string $term The term object, ID, or slug whose link will be retrieved. * @param string $taxonomy Optional. Taxonomy. Default empty. * @return string|WP_Error URL of the taxonomy term archive on success, WP_Error if term does not exist. */ function get_term_link( $term, $taxonomy = '' ) { global $wp_rewrite; if ( ! is_object( $term ) ) { if ( is_int( $term ) ) { $term = get_term( $term, $taxonomy ); } else { $term = get_term_by( 'slug', $term, $taxonomy ); } } if ( ! is_object( $term ) ) { $term = new WP_Error( 'invalid_term', __( 'Empty Term.' ) ); } if ( is_wp_error( $term ) ) { return $term; } $taxonomy = $term->taxonomy; $termlink = $wp_rewrite->get_extra_permastruct( $taxonomy ); /** * Filters the permalink structure for a term before token replacement occurs. * * @since 4.9.0 * * @param string $termlink The permalink structure for the term's taxonomy. * @param WP_Term $term The term object. */ $termlink = apply_filters( 'pre_term_link', $termlink, $term ); $slug = $term->slug; $t = get_taxonomy( $taxonomy ); if ( empty( $termlink ) ) { if ( 'category' === $taxonomy ) { $termlink = '?cat=' . $term->term_id; } elseif ( $t->query_var ) { $termlink = "?$t->query_var=$slug"; } else { $termlink = "?taxonomy=$taxonomy&term=$slug"; } $termlink = home_url( $termlink ); } else { if ( ! empty( $t->rewrite['hierarchical'] ) ) { $hierarchical_slugs = array(); $ancestors = get_ancestors( $term->term_id, $taxonomy, 'taxonomy' ); foreach ( (array) $ancestors as $ancestor ) { $ancestor_term = get_term( $ancestor, $taxonomy ); $hierarchical_slugs[] = $ancestor_term->slug; } $hierarchical_slugs = array_reverse( $hierarchical_slugs ); $hierarchical_slugs[] = $slug; $termlink = str_replace( "%$taxonomy%", implode( '/', $hierarchical_slugs ), $termlink ); } else { $termlink = str_replace( "%$taxonomy%", $slug, $termlink ); } $termlink = home_url( user_trailingslashit( $termlink, 'category' ) ); } // Back compat filters. if ( 'post_tag' === $taxonomy ) { /** * Filters the tag link. * * @since 2.3.0 * @since 2.5.0 Deprecated in favor of {@see 'term_link'} filter. * @since 5.4.1 Restored (un-deprecated). * * @param string $termlink Tag link URL. * @param int $term_id Term ID. */ $termlink = apply_filters( 'tag_link', $termlink, $term->term_id ); } elseif ( 'category' === $taxonomy ) { /** * Filters the category link. * * @since 1.5.0 * @since 2.5.0 Deprecated in favor of {@see 'term_link'} filter. * @since 5.4.1 Restored (un-deprecated). * * @param string $termlink Category link URL. * @param int $term_id Term ID. */ $termlink = apply_filters( 'category_link', $termlink, $term->term_id ); } /** * Filters the term link. * * @since 2.5.0 * * @param string $termlink Term link URL. * @param WP_Term $term Term object. * @param string $taxonomy Taxonomy slug. */ return apply_filters( 'term_link', $termlink, $term, $taxonomy ); } /** * Displays the taxonomies of a post with available options. * * This function can be used within the loop to display the taxonomies for a * post without specifying the Post ID. You can also use it outside the Loop to * display the taxonomies for a specific post. * * @since 2.5.0 * * @param array $args { * Arguments about which post to use and how to format the output. Shares all of the arguments * supported by get_the_taxonomies(), in addition to the following. * * @type int|WP_Post $post Post ID or object to get taxonomies of. Default current post. * @type string $before Displays before the taxonomies. Default empty string. * @type string $sep Separates each taxonomy. Default is a space. * @type string $after Displays after the taxonomies. Default empty string. * } */ function the_taxonomies( $args = array() ) { $defaults = array( 'post' => 0, 'before' => '', 'sep' => ' ', 'after' => '', ); $parsed_args = wp_parse_args( $args, $defaults ); echo $parsed_args['before'] . implode( $parsed_args['sep'], get_the_taxonomies( $parsed_args['post'], $parsed_args ) ) . $parsed_args['after']; } /** * Retrieves all taxonomies associated with a post. * * This function can be used within the loop. It will also return an array of * the taxonomies with links to the taxonomy and name. * * @since 2.5.0 * * @param int|WP_Post $post Optional. Post ID or WP_Post object. Default is global $post. * @param array $args { * Optional. Arguments about how to format the list of taxonomies. Default empty array. * * @type string $template Template for displaying a taxonomy label and list of terms. * Default is "Label: Terms." * @type string $term_template Template for displaying a single term in the list. Default is the term name * linked to its archive. * } * @return string[] List of taxonomies. */ function get_the_taxonomies( $post = 0, $args = array() ) { $post = get_post( $post ); $args = wp_parse_args( $args, array( /* translators: %s: Taxonomy label, %l: List of terms formatted as per $term_template. */ 'template' => __( '%s: %l.' ), 'term_template' => '%2$s', ) ); $taxonomies = array(); if ( ! $post ) { return $taxonomies; } foreach ( get_object_taxonomies( $post ) as $taxonomy ) { $t = (array) get_taxonomy( $taxonomy ); if ( empty( $t['label'] ) ) { $t['label'] = $taxonomy; } if ( empty( $t['args'] ) ) { $t['args'] = array(); } if ( empty( $t['template'] ) ) { $t['template'] = $args['template']; } if ( empty( $t['term_template'] ) ) { $t['term_template'] = $args['term_template']; } $terms = get_object_term_cache( $post->ID, $taxonomy ); if ( false === $terms ) { $terms = wp_get_object_terms( $post->ID, $taxonomy, $t['args'] ); } $links = array(); foreach ( $terms as $term ) { $links[] = wp_sprintf( $t['term_template'], esc_attr( get_term_link( $term ) ), $term->name ); } if ( $links ) { $taxonomies[ $taxonomy ] = wp_sprintf( $t['template'], $t['label'], $links, $terms ); } } return $taxonomies; } /** * Retrieves all taxonomy names for the given post. * * @since 2.5.0 * * @param int|WP_Post $post Optional. Post ID or WP_Post object. Default is global $post. * @return string[] An array of all taxonomy names for the given post. */ function get_post_taxonomies( $post = 0 ) { $post = get_post( $post ); return get_object_taxonomies( $post ); } /** * Determines if the given object is associated with any of the given terms. * * The given terms are checked against the object's terms' term_ids, names and slugs. * Terms given as integers will only be checked against the object's terms' term_ids. * If no terms are given, determines if object is associated with any terms in the given taxonomy. * * @since 2.7.0 * * @param int $object_id ID of the object (post ID, link ID, ...). * @param string $taxonomy Single taxonomy name. * @param int|string|int[]|string[] $terms Optional. Term ID, name, slug, or array of such * to check against. Default null. * @return bool|WP_Error WP_Error on input error. */ function is_object_in_term( $object_id, $taxonomy, $terms = null ) { $object_id = (int) $object_id; if ( ! $object_id ) { return new WP_Error( 'invalid_object', __( 'Invalid object ID.' ) ); } $object_terms = get_object_term_cache( $object_id, $taxonomy ); if ( false === $object_terms ) { $object_terms = wp_get_object_terms( $object_id, $taxonomy, array( 'update_term_meta_cache' => false ) ); if ( is_wp_error( $object_terms ) ) { return $object_terms; } wp_cache_set( $object_id, wp_list_pluck( $object_terms, 'term_id' ), "{$taxonomy}_relationships" ); } if ( is_wp_error( $object_terms ) ) { return $object_terms; } if ( empty( $object_terms ) ) { return false; } if ( empty( $terms ) ) { return ( ! empty( $object_terms ) ); } $terms = (array) $terms; $ints = array_filter( $terms, 'is_int' ); if ( $ints ) { $strs = array_diff( $terms, $ints ); } else { $strs =& $terms; } foreach ( $object_terms as $object_term ) { // If term is an int, check against term_ids only. if ( $ints && in_array( $object_term->term_id, $ints, true ) ) { return true; } if ( $strs ) { // Only check numeric strings against term_id, to avoid false matches due to type juggling. $numeric_strs = array_map( 'intval', array_filter( $strs, 'is_numeric' ) ); if ( in_array( $object_term->term_id, $numeric_strs, true ) ) { return true; } if ( in_array( $object_term->name, $strs, true ) ) { return true; } if ( in_array( $object_term->slug, $strs, true ) ) { return true; } } } return false; } /** * Determines if the given object type is associated with the given taxonomy. * * @since 3.0.0 * * @param string $object_type Object type string. * @param string $taxonomy Single taxonomy name. * @return bool True if object is associated with the taxonomy, otherwise false. */ function is_object_in_taxonomy( $object_type, $taxonomy ) { $taxonomies = get_object_taxonomies( $object_type ); if ( empty( $taxonomies ) ) { return false; } return in_array( $taxonomy, $taxonomies, true ); } /** * Gets an array of ancestor IDs for a given object. * * @since 3.1.0 * @since 4.1.0 Introduced the `$resource_type` argument. * * @param int $object_id Optional. The ID of the object. Default 0. * @param string $object_type Optional. The type of object for which we'll be retrieving * ancestors. Accepts a post type or a taxonomy name. Default empty. * @param string $resource_type Optional. Type of resource $object_type is. Accepts 'post_type' * or 'taxonomy'. Default empty. * @return int[] An array of IDs of ancestors from lowest to highest in the hierarchy. */ function get_ancestors( $object_id = 0, $object_type = '', $resource_type = '' ) { $object_id = (int) $object_id; $ancestors = array(); if ( empty( $object_id ) ) { /** This filter is documented in wp-includes/taxonomy.php */ return apply_filters( 'get_ancestors', $ancestors, $object_id, $object_type, $resource_type ); } if ( ! $resource_type ) { if ( is_taxonomy_hierarchical( $object_type ) ) { $resource_type = 'taxonomy'; } elseif ( post_type_exists( $object_type ) ) { $resource_type = 'post_type'; } } if ( 'taxonomy' === $resource_type ) { $term = get_term( $object_id, $object_type ); while ( ! is_wp_error( $term ) && ! empty( $term->parent ) && ! in_array( $term->parent, $ancestors, true ) ) { $ancestors[] = (int) $term->parent; $term = get_term( $term->parent, $object_type ); } } elseif ( 'post_type' === $resource_type ) { $ancestors = get_post_ancestors( $object_id ); } /** * Filters a given object's ancestors. * * @since 3.1.0 * @since 4.1.1 Introduced the `$resource_type` parameter. * * @param int[] $ancestors An array of IDs of object ancestors. * @param int $object_id Object ID. * @param string $object_type Type of object. * @param string $resource_type Type of resource $object_type is. */ return apply_filters( 'get_ancestors', $ancestors, $object_id, $object_type, $resource_type ); } /** * Returns the term's parent's term ID. * * @since 3.1.0 * * @param int $term_id Term ID. * @param string $taxonomy Taxonomy name. * @return int|false Parent term ID on success, false on failure. */ function wp_get_term_taxonomy_parent_id( $term_id, $taxonomy ) { $term = get_term( $term_id, $taxonomy ); if ( ! $term || is_wp_error( $term ) ) { return false; } return (int) $term->parent; } /** * Checks the given subset of the term hierarchy for hierarchy loops. * Prevents loops from forming and breaks those that it finds. * * Attached to the {@see 'wp_update_term_parent'} filter. * * @since 3.1.0 * * @param int $parent_term `term_id` of the parent for the term we're checking. * @param int $term_id The term we're checking. * @param string $taxonomy The taxonomy of the term we're checking. * @return int The new parent for the term. */ function wp_check_term_hierarchy_for_loops( $parent_term, $term_id, $taxonomy ) { // Nothing fancy here - bail. if ( ! $parent_term ) { return 0; } // Can't be its own parent. if ( $parent_term === $term_id ) { return 0; } // Now look for larger loops. $loop = wp_find_hierarchy_loop( 'wp_get_term_taxonomy_parent_id', $term_id, $parent_term, array( $taxonomy ) ); if ( ! $loop ) { return $parent_term; // No loop. } // Setting $parent_term to the given value causes a loop. if ( isset( $loop[ $term_id ] ) ) { return 0; } // There's a loop, but it doesn't contain $term_id. Break the loop. foreach ( array_keys( $loop ) as $loop_member ) { wp_update_term( $loop_member, $taxonomy, array( 'parent' => 0 ) ); } return $parent_term; } /** * Determines whether a taxonomy is considered "viewable". * * @since 5.1.0 * * @param string|WP_Taxonomy $taxonomy Taxonomy name or object. * @return bool Whether the taxonomy should be considered viewable. */ function is_taxonomy_viewable( $taxonomy ) { if ( is_scalar( $taxonomy ) ) { $taxonomy = get_taxonomy( $taxonomy ); if ( ! $taxonomy ) { return false; } } return $taxonomy->publicly_queryable; } /** * Determines whether a term is publicly viewable. * * A term is considered publicly viewable if its taxonomy is viewable. * * @since 6.1.0 * * @param int|WP_Term $term Term ID or term object. * @return bool Whether the term is publicly viewable. */ function is_term_publicly_viewable( $term ) { $term = get_term( $term ); if ( ! $term ) { return false; } return is_taxonomy_viewable( $term->taxonomy ); } /** * Sets the last changed time for the 'terms' cache group. * * @since 5.0.0 */ function wp_cache_set_terms_last_changed() { wp_cache_set_last_changed( 'terms' ); } /** * Aborts calls to term meta if it is not supported. * * @since 5.0.0 * * @param mixed $check Skip-value for whether to proceed term meta function execution. * @return mixed Original value of $check, or false if term meta is not supported. */ function wp_check_term_meta_support_prefilter( $check ) { if ( get_option( 'db_version' ) < 34370 ) { return false; } return $check; } LT_TRANSPORTS; } self::$transports[$transport] = $transport; } /** * Get the fully qualified class name (FQCN) for a working transport. * * @param array $capabilities Optional. Associative array of capabilities to test against, i.e. `['' => true]`. * @return string FQCN of the transport to use, or an empty string if no transport was * found which provided the requested capabilities. */ protected static function get_transport_class(array $capabilities = []) { // Caching code, don't bother testing coverage. // @codeCoverageIgnoreStart // Array of capabilities as a string to be used as an array key. ksort($capabilities); $cap_string = serialize($capabilities); // Don't search for a transport if it's already been done for these $capabilities. if (isset(self::$transport[$cap_string])) { return self::$transport[$cap_string]; } // Ensure we will not run this same check again later on. self::$transport[$cap_string] = ''; // @codeCoverageIgnoreEnd if (empty(self::$transports)) { self::$transports = self::DEFAULT_TRANSPORTS; } // Find us a working transport. foreach (self::$transports as $class) { if (!class_exists($class)) { continue; } $result = $class::test($capabilities); if ($result === true) { self::$transport[$cap_string] = $class; break; } } return self::$transport[$cap_string]; } /** * Get a working transport. * * @param array $capabilities Optional. Associative array of capabilities to test against, i.e. `['' => true]`. * @return \WpOrg\Requests\Transport * @throws \WpOrg\Requests\Exception If no valid transport is found (`notransport`). */ protected static function get_transport(array $capabilities = []) { $class = self::get_transport_class($capabilities); if ($class === '') { throw new Exception('No working transports found', 'notransport', self::$transports); } return new $class(); } /** * Checks to see if we have a transport for the capabilities requested. * * Supported capabilities can be found in the {@see \WpOrg\Requests\Capability} * interface as constants. * * Example usage: * `Requests::has_capabilities([Capability::SSL => true])`. * * @param array $capabilities Optional. Associative array of capabilities to test against, i.e. `['' => true]`. * @return bool Whether the transport has the requested capabilities. */ public static function has_capabilities(array $capabilities = []) { return self::get_transport_class($capabilities) !== ''; } /**#@+ * @see \WpOrg\Requests\Requests::request() * @param string $url * @param array $headers * @param array $options * @return \WpOrg\Requests\Response */ /** * Send a GET request */ public static function get($url, $headers = [], $options = []) { return self::request($url, $headers, null, self::GET, $options); } /** * Send a HEAD request */ public static function head($url, $headers = [], $options = []) { return self::request($url, $headers, null, self::HEAD, $options); } /** * Send a DELETE request */ public static function delete($url, $headers = [], $options = []) { return self::request($url, $headers, null, self::DELETE, $options); } /** * Send a TRACE request */ public static function trace($url, $headers = [], $options = []) { return self::request($url, $headers, null, self::TRACE, $options); } /**#@-*/ /**#@+ * @see \WpOrg\Requests\Requests::request() * @param string $url * @param array $headers * @param array $data * @param array $options * @return \WpOrg\Requests\Response */ /** * Send a POST request */ public static function post($url, $headers = [], $data = [], $options = []) { return self::request($url, $headers, $data, self::POST, $options); } /** * Send a PUT request */ public static function put($url, $headers = [], $data = [], $options = []) { return self::request($url, $headers, $data, self::PUT, $options); } /** * Send an OPTIONS request */ public static function options($url, $headers = [], $data = [], $options = []) { return self::request($url, $headers, $data, self::OPTIONS, $options); } /** * Send a PATCH request * * Note: Unlike {@see \WpOrg\Requests\Requests::post()} and {@see \WpOrg\Requests\Requests::put()}, * `$headers` is required, as the specification recommends that should send an ETag * * @link https://tools.ietf.org/html/rfc5789 */ public static function patch($url, $headers, $data = [], $options = []) { return self::request($url, $headers, $data, self::PATCH, $options); } /**#@-*/ /** * Main interface for HTTP requests * * This method initiates a request and sends it via a transport before * parsing. * * The `$options` parameter takes an associative array with the following * options: * * - `timeout`: How long should we wait for a response? * Note: for cURL, a minimum of 1 second applies, as DNS resolution * operates at second-resolution only. * (float, seconds with a millisecond precision, default: 10, example: 0.01) * - `connect_timeout`: How long should we wait while trying to connect? * (float, seconds with a millisecond precision, default: 10, example: 0.01) * - `useragent`: Useragent to send to the server * (string, default: php-requests/$version) * - `follow_redirects`: Should we follow 3xx redirects? * (boolean, default: true) * - `redirects`: How many times should we redirect before erroring? * (integer, default: 10) * - `blocking`: Should we block processing on this request? * (boolean, default: true) * - `filename`: File to stream the body to instead. * (string|boolean, default: false) * - `auth`: Authentication handler or array of user/password details to use * for Basic authentication * (\WpOrg\Requests\Auth|array|boolean, default: false) * - `proxy`: Proxy details to use for proxy by-passing and authentication * (\WpOrg\Requests\Proxy|array|string|boolean, default: false) * - `max_bytes`: Limit for the response body size. * (integer|boolean, default: false) * - `idn`: Enable IDN parsing * (boolean, default: true) * - `transport`: Custom transport. Either a class name, or a * transport object. Defaults to the first working transport from * {@see \WpOrg\Requests\Requests::getTransport()} * (string|\WpOrg\Requests\Transport, default: {@see \WpOrg\Requests\Requests::getTransport()}) * - `hooks`: Hooks handler. * (\WpOrg\Requests\HookManager, default: new WpOrg\Requests\Hooks()) * - `verify`: Should we verify SSL certificates? Allows passing in a custom * certificate file as a string. (Using true uses the system-wide root * certificate store instead, but this may have different behaviour * across transports.) * (string|boolean, default: certificates/cacert.pem) * - `verifyname`: Should we verify the common name in the SSL certificate? * (boolean, default: true) * - `data_format`: How should we send the `$data` parameter? * (string, one of 'query' or 'body', default: 'query' for * HEAD/GET/DELETE, 'body' for POST/PUT/OPTIONS/PATCH) * * @param string|Stringable $url URL to request * @param array $headers Extra headers to send with the request * @param array|null $data Data to send either as a query string for GET/HEAD requests, or in the body for POST requests * @param string $type HTTP request type (use Requests constants) * @param array $options Options for the request (see description for more information) * @return \WpOrg\Requests\Response * * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $url argument is not a string or Stringable. * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $type argument is not a string. * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $options argument is not an array. * @throws \WpOrg\Requests\Exception On invalid URLs (`nonhttp`) */ public static function request($url, $headers = [], $data = [], $type = self::GET, $options = []) { if (InputValidator::is_string_or_stringable($url) === false) { throw InvalidArgument::create(1, '$url', 'string|Stringable', gettype($url)); } if (is_string($type) === false) { throw InvalidArgument::create(4, '$type', 'string', gettype($type)); } if (is_array($options) === false) { throw InvalidArgument::create(5, '$options', 'array', gettype($options)); } if (empty($options['type'])) { $options['type'] = $type; } $options = array_merge(self::get_default_options(), $options); self::set_defaults($url, $headers, $data, $type, $options); $options['hooks']->dispatch('requests.before_request', [&$url, &$headers, &$data, &$type, &$options]); if (!empty($options['transport'])) { $transport = $options['transport']; if (is_string($options['transport'])) { $transport = new $transport(); } } else { $need_ssl = (stripos($url, 'https://') === 0); $capabilities = [Capability::SSL => $need_ssl]; $transport = self::get_transport($capabilities); } $response = $transport->request($url, $headers, $data, $options); $options['hooks']->dispatch('requests.before_parse', [&$response, $url, $headers, $data, $type, $options]); return self::parse_response($response, $url, $headers, $data, $options); } /** * Send multiple HTTP requests simultaneously * * The `$requests` parameter takes an associative or indexed array of * request fields. The key of each request can be used to match up the * request with the returned data, or with the request passed into your * `multiple.request.complete` callback. * * The request fields value is an associative array with the following keys: * * - `url`: Request URL Same as the `$url` parameter to * {@see \WpOrg\Requests\Requests::request()} * (string, required) * - `headers`: Associative array of header fields. Same as the `$headers` * parameter to {@see \WpOrg\Requests\Requests::request()} * (array, default: `array()`) * - `data`: Associative array of data fields or a string. Same as the * `$data` parameter to {@see \WpOrg\Requests\Requests::request()} * (array|string, default: `array()`) * - `type`: HTTP request type (use \WpOrg\Requests\Requests constants). Same as the `$type` * parameter to {@see \WpOrg\Requests\Requests::request()} * (string, default: `\WpOrg\Requests\Requests::GET`) * - `cookies`: Associative array of cookie name to value, or cookie jar. * (array|\WpOrg\Requests\Cookie\Jar) * * If the `$options` parameter is specified, individual requests will * inherit options from it. This can be used to use a single hooking system, * or set all the types to `\WpOrg\Requests\Requests::POST`, for example. * * In addition, the `$options` parameter takes the following global options: * * - `complete`: A callback for when a request is complete. Takes two * parameters, a \WpOrg\Requests\Response/\WpOrg\Requests\Exception reference, and the * ID from the request array (Note: this can also be overridden on a * per-request basis, although that's a little silly) * (callback) * * @param array $requests Requests data (see description for more information) * @param array $options Global and default options (see {@see \WpOrg\Requests\Requests::request()}) * @return array Responses (either \WpOrg\Requests\Response or a \WpOrg\Requests\Exception object) * * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $requests argument is not an array or iterable object with array access. * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $options argument is not an array. */ public static function request_multiple($requests, $options = []) { if (InputValidator::has_array_access($requests) === false || InputValidator::is_iterable($requests) === false) { throw InvalidArgument::create(1, '$requests', 'array|ArrayAccess&Traversable', gettype($requests)); } if (is_array($options) === false) { throw InvalidArgument::create(2, '$options', 'array', gettype($options)); } $options = array_merge(self::get_default_options(true), $options); if (!empty($options['hooks'])) { $options['hooks']->register('transport.internal.parse_response', [static::class, 'parse_multiple']); if (!empty($options['complete'])) { $options['hooks']->register('multiple.request.complete', $options['complete']); } } foreach ($requests as $id => &$request) { if (!isset($request['headers'])) { $request['headers'] = []; } if (!isset($request['data'])) { $request['data'] = []; } if (!isset($request['type'])) { $request['type'] = self::GET; } if (!isset($request['options'])) { $request['options'] = $options; $request['options']['type'] = $request['type']; } else { if (empty($request['options']['type'])) { $request['options']['type'] = $request['type']; } $request['options'] = array_merge($options, $request['options']); } self::set_defaults($request['url'], $request['headers'], $request['data'], $request['type'], $request['options']); // Ensure we only hook in once if ($request['options']['hooks'] !== $options['hooks']) { $request['options']['hooks']->register('transport.internal.parse_response', [static::class, 'parse_multiple']); if (!empty($request['options']['complete'])) { $request['options']['hooks']->register('multiple.request.complete', $request['options']['complete']); } } } unset($request); if (!empty($options['transport'])) { $transport = $options['transport']; if (is_string($options['transport'])) { $transport = new $transport(); } } else { $transport = self::get_transport(); } $responses = $transport->request_multiple($requests, $options); foreach ($responses as $id => &$response) { // If our hook got messed with somehow, ensure we end up with the // correct response if (is_string($response)) { $request = $requests[$id]; self::parse_multiple($response, $request); $request['options']['hooks']->dispatch('multiple.request.complete', [&$response, $id]); } } return $responses; } /** * Get the default options * * @see \WpOrg\Requests\Requests::request() for values returned by this method * @param boolean $multirequest Is this a multirequest? * @return array Default option values */ protected static function get_default_options($multirequest = false) { $defaults = static::OPTION_DEFAULTS; $defaults['verify'] = self::$certificate_path; if ($multirequest !== false) { $defaults['complete'] = null; } return $defaults; } /** * Get default certificate path. * * @return string Default certificate path. */ public static function get_certificate_path() { return self::$certificate_path; } /** * Set default certificate path. * * @param string|Stringable|bool $path Certificate path, pointing to a PEM file. * * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $url argument is not a string, Stringable or boolean. */ public static function set_certificate_path($path) { if (InputValidator::is_string_or_stringable($path) === false && is_bool($path) === false) { throw InvalidArgument::create(1, '$path', 'string|Stringable|bool', gettype($path)); } self::$certificate_path = $path; } /** * Set the default values * * The $options parameter is updated with the results. * * @param string $url URL to request * @param array $headers Extra headers to send with the request * @param array|null $data Data to send either as a query string for GET/HEAD requests, or in the body for POST requests * @param string $type HTTP request type * @param array $options Options for the request * @return void * * @throws \WpOrg\Requests\Exception When the $url is not an http(s) URL. */ protected static function set_defaults(&$url, &$headers, &$data, &$type, &$options) { if (!preg_match('/^http(s)?:\/\//i', $url, $matches)) { throw new Exception('Only HTTP(S) requests are handled.', 'nonhttp', $url); } if (empty($options['hooks'])) { $options['hooks'] = new Hooks(); } if (is_array($options['auth'])) { $options['auth'] = new Basic($options['auth']); } if ($options['auth'] !== false) { $options['auth']->register($options['hooks']); } if (is_string($options['proxy']) || is_array($options['proxy'])) { $options['proxy'] = new Http($options['proxy']); } if ($options['proxy'] !== false) { $options['proxy']->register($options['hooks']); } if (is_array($options['cookies'])) { $options['cookies'] = new Jar($options['cookies']); } elseif (empty($options['cookies'])) { $options['cookies'] = new Jar(); } if ($options['cookies'] !== false) { $options['cookies']->register($options['hooks']); } if ($options['idn'] !== false) { $iri = new Iri($url); $iri->host = IdnaEncoder::encode($iri->ihost); $url = $iri->uri; } // Massage the type to ensure we support it. $type = strtoupper($type); if (!isset($options['data_format'])) { if (in_array($type, [self::HEAD, self::GET, self::DELETE], true)) { $options['data_format'] = 'query'; } else { $options['data_format'] = 'body'; } } } /** * HTTP response parser * * @param string $headers Full response text including headers and body * @param string $url Original request URL * @param array $req_headers Original $headers array passed to {@link request()}, in case we need to follow redirects * @param array $req_data Original $data array passed to {@link request()}, in case we need to follow redirects * @param array $options Original $options array passed to {@link request()}, in case we need to follow redirects * @return \WpOrg\Requests\Response * * @throws \WpOrg\Requests\Exception On missing head/body separator (`requests.no_crlf_separator`) * @throws \WpOrg\Requests\Exception On missing head/body separator (`noversion`) * @throws \WpOrg\Requests\Exception On missing head/body separator (`toomanyredirects`) */ protected static function parse_response($headers, $url, $req_headers, $req_data, $options) { $return = new Response(); if (!$options['blocking']) { return $return; } $return->raw = $headers; $return->url = (string) $url; $return->body = ''; if (!$options['filename']) { $pos = strpos($headers, "\r\n\r\n"); if ($pos === false) { // Crap! throw new Exception('Missing header/body separator', 'requests.no_crlf_separator'); } $headers = substr($return->raw, 0, $pos); // Headers will always be separated from the body by two new lines - `\n\r\n\r`. $body = substr($return->raw, $pos + 4); if (!empty($body)) { $return->body = $body; } } // Pretend CRLF = LF for compatibility (RFC 2616, section 19.3) $headers = str_replace("\r\n", "\n", $headers); // Unfold headers (replace [CRLF] 1*( SP | HT ) with SP) as per RFC 2616 (section 2.2) $headers = preg_replace('/\n[ \t]/', ' ', $headers); $headers = explode("\n", $headers); preg_match('#^HTTP/(1\.\d)[ \t]+(\d+)#i', array_shift($headers), $matches); if (empty($matches)) { throw new Exception('Response could not be parsed', 'noversion', $headers); } $return->protocol_version = (float) $matches[1]; $return->status_code = (int) $matches[2]; if ($return->status_code >= 200 && $return->status_code < 300) { $return->success = true; } foreach ($headers as $header) { list($key, $value) = explode(':', $header, 2); $value = trim($value); preg_replace('#(\s+)#i', ' ', $value); $return->headers[$key] = $value; } if (isset($return->headers['transfer-encoding'])) { $return->body = self::decode_chunked($return->body); unset($return->headers['transfer-encoding']); } if (isset($return->headers['content-encoding'])) { $return->body = self::decompress($return->body); } //fsockopen and cURL compatibility if (isset($return->headers['connection'])) { unset($return->headers['connection']); } $options['hooks']->dispatch('requests.before_redirect_check', [&$return, $req_headers, $req_data, $options]); if ($return->is_redirect() && $options['follow_redirects'] === true) { if (isset($return->headers['location']) && $options['redirected'] < $options['redirects']) { if ($return->status_code === 303) { $options['type'] = self::GET; } $options['redirected']++; $location = $return->headers['location']; if (strpos($location, 'http://') !== 0 && strpos($location, 'https://') !== 0) { // relative redirect, for compatibility make it absolute $location = Iri::absolutize($url, $location); $location = $location->uri; } $hook_args = [ &$location, &$req_headers, &$req_data, &$options, $return, ]; $options['hooks']->dispatch('requests.before_redirect', $hook_args); $redirected = self::request($location, $req_headers, $req_data, $options['type'], $options); $redirected->history[] = $return; return $redirected; } elseif ($options['redirected'] >= $options['redirects']) { throw new Exception('Too many redirects', 'toomanyredirects', $return); } } $return->redirects = $options['redirected']; $options['hooks']->dispatch('requests.after_request', [&$return, $req_headers, $req_data, $options]); return $return; } /** * Callback for `transport.internal.parse_response` * * Internal use only. Converts a raw HTTP response to a \WpOrg\Requests\Response * while still executing a multiple request. * * `$response` is either set to a \WpOrg\Requests\Response instance, or a \WpOrg\Requests\Exception object * * @param string $response Full response text including headers and body (will be overwritten with Response instance) * @param array $request Request data as passed into {@see \WpOrg\Requests\Requests::request_multiple()} * @return void */ public static function parse_multiple(&$response, $request) { try { $url = $request['url']; $headers = $request['headers']; $data = $request['data']; $options = $request['options']; $response = self::parse_response($response, $url, $headers, $data, $options); } catch (Exception $e) { $response = $e; } } /** * Decoded a chunked body as per RFC 2616 * * @link https://tools.ietf.org/html/rfc2616#section-3.6.1 * @param string $data Chunked body * @return string Decoded body */ protected static function decode_chunked($data) { if (!preg_match('/^([0-9a-f]+)(?:;(?:[\w-]*)(?:=(?:(?:[\w-]*)*|"(?:[^\r\n])*"))?)*\r\n/i', trim($data))) { return $data; } $decoded = ''; $encoded = $data; while (true) { $is_chunked = (bool) preg_match('/^([0-9a-f]+)(?:;(?:[\w-]*)(?:=(?:(?:[\w-]*)*|"(?:[^\r\n])*"))?)*\r\n/i', $encoded, $matches); if (!$is_chunked) { // Looks like it's not chunked after all return $data; } $length = hexdec(trim($matches[1])); if ($length === 0) { // Ignore trailer headers return $decoded; } $chunk_length = strlen($matches[0]); $decoded .= substr($encoded, $chunk_length, $length); $encoded = substr($encoded, $chunk_length + $length + 2); if (trim($encoded) === '0' || empty($encoded)) { return $decoded; } } // We'll never actually get down here // @codeCoverageIgnoreStart } // @codeCoverageIgnoreEnd /** * Convert a key => value array to a 'key: value' array for headers * * @param iterable $dictionary Dictionary of header values * @return array List of headers * * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed argument is not iterable. */ public static function flatten($dictionary) { if (InputValidator::is_iterable($dictionary) === false) { throw InvalidArgument::create(1, '$dictionary', 'iterable', gettype($dictionary)); } $return = []; foreach ($dictionary as $key => $value) { $return[] = sprintf('%s: %s', $key, $value); } return $return; } /** * Decompress an encoded body * * Implements gzip, compress and deflate. Guesses which it is by attempting * to decode. * * @param string $data Compressed data in one of the above formats * @return string Decompressed string * * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed argument is not a string. */ public static function decompress($data) { if (is_string($data) === false) { throw InvalidArgument::create(1, '$data', 'string', gettype($data)); } if (trim($data) === '') { // Empty body does not need further processing. return $data; } $marker = substr($data, 0, 2); if (!isset(self::$magic_compression_headers[$marker])) { // Not actually compressed. Probably cURL ruining this for us. return $data; } if (function_exists('gzdecode')) { $decoded = @gzdecode($data); if ($decoded !== false) { return $decoded; } } if (function_exists('gzinflate')) { $decoded = @gzinflate($data); if ($decoded !== false) { return $decoded; } } $decoded = self::compatible_gzinflate($data); if ($decoded !== false) { return $decoded; } if (function_exists('gzuncompress')) { $decoded = @gzuncompress($data); if ($decoded !== false) { return $decoded; } } return $data; } /** * Decompression of deflated string while staying compatible with the majority of servers. * * Certain Servers will return deflated data with headers which PHP's gzinflate() * function cannot handle out of the box. The following function has been created from * various snippets on the gzinflate() PHP documentation. * * Warning: Magic numbers within. Due to the potential different formats that the compressed * data may be returned in, some "magic offsets" are needed to ensure proper decompression * takes place. For a simple progmatic way to determine the magic offset in use, see: * https://core.trac.wordpress.org/ticket/18273 * * @since 1.6.0 * @link https://core.trac.wordpress.org/ticket/18273 * @link https://www.php.net/gzinflate#70875 * @link https://www.php.net/gzinflate#77336 * * @param string $gz_data String to decompress. * @return string|bool False on failure. * * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed argument is not a string. */ public static function compatible_gzinflate($gz_data) { if (is_string($gz_data) === false) { throw InvalidArgument::create(1, '$gz_data', 'string', gettype($gz_data)); } if (trim($gz_data) === '') { return false; } // Compressed data might contain a full zlib header, if so strip it for // gzinflate() if (substr($gz_data, 0, 3) === "\x1f\x8b\x08") { $i = 10; $flg = ord(substr($gz_data, 3, 1)); if ($flg > 0) { if ($flg & 4) { list($xlen) = unpack('v', substr($gz_data, $i, 2)); $i += 2 + $xlen; } if ($flg & 8) { $i = strpos($gz_data, "\0", $i) + 1; } if ($flg & 16) { $i = strpos($gz_data, "\0", $i) + 1; } if ($flg & 2) { $i += 2; } } $decompressed = self::compatible_gzinflate(substr($gz_data, $i)); if ($decompressed !== false) { return $decompressed; } } // If the data is Huffman Encoded, we must first strip the leading 2 // byte Huffman marker for gzinflate() // The response is Huffman coded by many compressors such as // java.util.zip.Deflater, Ruby's Zlib::Deflate, and .NET's // System.IO.Compression.DeflateStream. // // See https://decompres.blogspot.com/ for a quick explanation of this // data type $huffman_encoded = false; // low nibble of first byte should be 0x08 list(, $first_nibble) = unpack('h', $gz_data); // First 2 bytes should be divisible by 0x1F list(, $first_two_bytes) = unpack('n', $gz_data); if ($first_nibble === 0x08 && ($first_two_bytes % 0x1F) === 0) { $huffman_encoded = true; } if ($huffman_encoded) { $decompressed = @gzinflate(substr($gz_data, 2)); if ($decompressed !== false) { return $decompressed; } } if (substr($gz_data, 0, 4) === "\x50\x4b\x03\x04") { // ZIP file format header // Offset 6: 2 bytes, General-purpose field // Offset 26: 2 bytes, filename length // Offset 28: 2 bytes, optional field length // Offset 30: Filename field, followed by optional field, followed // immediately by data list(, $general_purpose_flag) = unpack('v', substr($gz_data, 6, 2)); // If the file has been compressed on the fly, 0x08 bit is set of // the general purpose field. We can use this to differentiate // between a compressed document, and a ZIP file $zip_compressed_on_the_fly = ((0x08 & $general_purpose_flag) === 0x08); if (!$zip_compressed_on_the_fly) { // Don't attempt to decode a compressed zip file return $gz_data; } // Determine the first byte of data, based on the above ZIP header // offsets: $first_file_start = array_sum(unpack('v2', substr($gz_data, 26, 4))); $decompressed = @gzinflate(substr($gz_data, 30 + $first_file_start)); if ($decompressed !== false) { return $decompressed; } return false; } // Finally fall back to straight gzinflate $decompressed = @gzinflate($gz_data); if ($decompressed !== false) { return $decompressed; } // Fallback for all above failing, not expected, but included for // debugging and preventing regressions and to track stats $decompressed = @gzinflate(substr($gz_data, 2)); if ($decompressed !== false) { return $decompressed; } return false; } }