ic::ROOT_BLOCK_SELECTOR === $selector ) { // Classes at the global level do not need any CSS prefixed, // and we don't want to increase its specificity. $selector = ''; } $stylesheet = ''; foreach ( static::PRESETS_METADATA as $preset_metadata ) { $slugs = static::get_settings_slugs( $settings, $preset_metadata, $origins ); foreach ( $preset_metadata['classes'] as $class => $property ) { foreach ( $slugs as $slug ) { $css_var = static::replace_slug_in_string( $preset_metadata['css_vars'], $slug ); $class_name = static::replace_slug_in_string( $class, $slug ); $stylesheet .= static::to_ruleset( static::append_to_selector( $selector, $class_name ), array( array( 'name' => $property, 'value' => 'var(' . $css_var . ') !important', ), ) ); } } } return $stylesheet; } /** * Function that scopes a selector with another one. This works a bit like * SCSS nesting except the `&` operator isn't supported. * * * $scope = '.a, .b .c'; * $selector = '> .x, .y'; * $merged = scope_selector( $scope, $selector ); * // $merged is '.a > .x, .a .y, .b .c > .x, .b .c .y' * * * @since 5.9.0 * * @param string $scope Selector to scope to. * @param string $selector Original selector. * @return string Scoped selector. */ protected static function scope_selector( $scope, $selector ) { $scopes = explode( ',', $scope ); $selectors = explode( ',', $selector ); $selectors_scoped = array(); foreach ( $scopes as $outer ) { foreach ( $selectors as $inner ) { $selectors_scoped[] = trim( $outer ) . ' ' . trim( $inner ); } } return implode( ', ', $selectors_scoped ); } /** * Gets preset values keyed by slugs based on settings and metadata. * * * $settings = array( * 'typography' => array( * 'fontFamilies' => array( * array( * 'slug' => 'sansSerif', * 'fontFamily' => '"Helvetica Neue", sans-serif', * ), * array( * 'slug' => 'serif', * 'colors' => 'Georgia, serif', * ) * ), * ), * ); * $meta = array( * 'path' => array( 'typography', 'fontFamilies' ), * 'value_key' => 'fontFamily', * ); * $values_by_slug = get_settings_values_by_slug(); * // $values_by_slug === array( * // 'sans-serif' => '"Helvetica Neue", sans-serif', * // 'serif' => 'Georgia, serif', * // ); * * * @since 5.9.0 * * @param array $settings Settings to process. * @param array $preset_metadata One of the PRESETS_METADATA values. * @param array $origins List of origins to process. * @return array Array of presets where each key is a slug and each value is the preset value. */ protected static function get_settings_values_by_slug( $settings, $preset_metadata, $origins ) { $preset_per_origin = _wp_array_get( $settings, $preset_metadata['path'], array() ); $result = array(); foreach ( $origins as $origin ) { if ( ! isset( $preset_per_origin[ $origin ] ) ) { continue; } foreach ( $preset_per_origin[ $origin ] as $preset ) { $slug = _wp_to_kebab_case( $preset['slug'] ); $value = ''; if ( isset( $preset_metadata['value_key'], $preset[ $preset_metadata['value_key'] ] ) ) { $value_key = $preset_metadata['value_key']; $value = $preset[ $value_key ]; } elseif ( isset( $preset_metadata['value_func'] ) && is_callable( $preset_metadata['value_func'] ) ) { $value_func = $preset_metadata['value_func']; $value = call_user_func( $value_func, $preset ); } else { // If we don't have a value, then don't add it to the result. continue; } $result[ $slug ] = $value; } } return $result; } /** * Similar to get_settings_values_by_slug, but doesn't compute the value. * * @since 5.9.0 * * @param array $settings Settings to process. * @param array $preset_metadata One of the PRESETS_METADATA values. * @param array $origins List of origins to process. * @return array Array of presets where the key and value are both the slug. */ protected static function get_settings_slugs( $settings, $preset_metadata, $origins = null ) { if ( null === $origins ) { $origins = static::VALID_ORIGINS; } $preset_per_origin = _wp_array_get( $settings, $preset_metadata['path'], array() ); $result = array(); foreach ( $origins as $origin ) { if ( ! isset( $preset_per_origin[ $origin ] ) ) { continue; } foreach ( $preset_per_origin[ $origin ] as $preset ) { $slug = _wp_to_kebab_case( $preset['slug'] ); // Use the array as a set so we don't get duplicates. $result[ $slug ] = $slug; } } return $result; } /** * Transform a slug into a CSS Custom Property. * * @since 5.9.0 * * @param string $input String to replace. * @param string $slug The slug value to use to generate the custom property. * @return string The CSS Custom Property. Something along the lines of `--wp--preset--color--black`. */ protected static function replace_slug_in_string( $input, $slug ) { return strtr( $input, array( '$slug' => $slug ) ); } /** * Given the block settings, it extracts the CSS Custom Properties * for the presets and adds them to the $declarations array * following the format: * * array( * 'name' => 'property_name', * 'value' => 'property_value, * ) * * @since 5.8.0 * @since 5.9.0 Added the `$origins` parameter. * * @param array $settings Settings to process. * @param array $origins List of origins to process. * @return array Returns the modified $declarations. */ protected static function compute_preset_vars( $settings, $origins ) { $declarations = array(); foreach ( static::PRESETS_METADATA as $preset_metadata ) { $values_by_slug = static::get_settings_values_by_slug( $settings, $preset_metadata, $origins ); foreach ( $values_by_slug as $slug => $value ) { $declarations[] = array( 'name' => static::replace_slug_in_string( $preset_metadata['css_vars'], $slug ), 'value' => $value, ); } } return $declarations; } /** * Given an array of settings, it extracts the CSS Custom Properties * for the custom values and adds them to the $declarations * array following the format: * * array( * 'name' => 'property_name', * 'value' => 'property_value, * ) * * @since 5.8.0 * * @param array $settings Settings to process. * @return array Returns the modified $declarations. */ protected static function compute_theme_vars( $settings ) { $declarations = array(); $custom_values = _wp_array_get( $settings, array( 'custom' ), array() ); $css_vars = static::flatten_tree( $custom_values ); foreach ( $css_vars as $key => $value ) { $declarations[] = array( 'name' => '--wp--custom--' . $key, 'value' => $value, ); } return $declarations; } /** * Given a tree, it creates a flattened one * by merging the keys and binding the leaf values * to the new keys. * * It also transforms camelCase names into kebab-case * and substitutes '/' by '-'. * * This is thought to be useful to generate * CSS Custom Properties from a tree, * although there's nothing in the implementation * of this function that requires that format. * * For example, assuming the given prefix is '--wp' * and the token is '--', for this input tree: * * { * 'some/property': 'value', * 'nestedProperty': { * 'sub-property': 'value' * } * } * * it'll return this output: * * { * '--wp--some-property': 'value', * '--wp--nested-property--sub-property': 'value' * } * * @since 5.8.0 * * @param array $tree Input tree to process. * @param string $prefix Optional. Prefix to prepend to each variable. Default empty string. * @param string $token Optional. Token to use between levels. Default '--'. * @return array The flattened tree. */ protected static function flatten_tree( $tree, $prefix = '', $token = '--' ) { $result = array(); foreach ( $tree as $property => $value ) { $new_key = $prefix . str_replace( '/', '-', strtolower( _wp_to_kebab_case( $property ) ) ); if ( is_array( $value ) ) { $new_prefix = $new_key . $token; $result = array_merge( $result, static::flatten_tree( $value, $new_prefix, $token ) ); } else { $result[ $new_key ] = $value; } } return $result; } /** * Given a styles array, it extracts the style properties * and adds them to the $declarations array following the format: * * array( * 'name' => 'property_name', * 'value' => 'property_value, * ) * * @since 5.8.0 * @since 5.9.0 Added the `$settings` and `$properties` parameters. * * @param array $styles Styles to process. * @param array $settings Theme settings. * @param array $properties Properties metadata. * @return array Returns the modified $declarations. */ protected static function compute_style_properties( $styles, $settings = array(), $properties = null ) { if ( null === $properties ) { $properties = static::PROPERTIES_METADATA; } $declarations = array(); if ( empty( $styles ) ) { return $declarations; } foreach ( $properties as $css_property => $value_path ) { $value = static::get_property_value( $styles, $value_path ); // Look up protected properties, keyed by value path. // Skip protected properties that are explicitly set to `null`. if ( is_array( $value_path ) ) { $path_string = implode( '.', $value_path ); if ( array_key_exists( $path_string, static::PROTECTED_PROPERTIES ) && _wp_array_get( $settings, static::PROTECTED_PROPERTIES[ $path_string ], null ) === null ) { continue; } } // Skip if empty and not "0" or value represents array of longhand values. $has_missing_value = empty( $value ) && ! is_numeric( $value ); if ( $has_missing_value || is_array( $value ) ) { continue; } $declarations[] = array( 'name' => $css_property, 'value' => $value, ); } return $declarations; } /** * Returns the style property for the given path. * * It also converts CSS Custom Property stored as * "var:preset|color|secondary" to the form * "--wp--preset--color--secondary". * * @since 5.8.0 * @since 5.9.0 Added support for values of array type, which are returned as is. * * @param array $styles Styles subtree. * @param array $path Which property to process. * @return string|array Style property value. */ protected static function get_property_value( $styles, $path ) { $value = _wp_array_get( $styles, $path, '' ); if ( '' === $value || is_array( $value ) ) { return $value; } $prefix = 'var:'; $prefix_len = strlen( $prefix ); $token_in = '|'; $token_out = '--'; if ( 0 === strncmp( $value, $prefix, $prefix_len ) ) { $unwrapped_name = str_replace( $token_in, $token_out, substr( $value, $prefix_len ) ); $value = "var(--wp--$unwrapped_name)"; } return $value; } /** * Builds metadata for the setting nodes, which returns in the form of: * * [ * [ * 'path' => ['path', 'to', 'some', 'node' ], * 'selector' => 'CSS selector for some node' * ], * [ * 'path' => [ 'path', 'to', 'other', 'node' ], * 'selector' => 'CSS selector for other node' * ], * ] * * @since 5.8.0 * * @param array $theme_json The tree to extract setting nodes from. * @param array $selectors List of selectors per block. * @return array */ protected static function get_setting_nodes( $theme_json, $selectors = array() ) { $nodes = array(); if ( ! isset( $theme_json['settings'] ) ) { return $nodes; } // Top-level. $nodes[] = array( 'path' => array( 'settings' ), 'selector' => static::ROOT_BLOCK_SELECTOR, ); // Calculate paths for blocks. if ( ! isset( $theme_json['settings']['blocks'] ) ) { return $nodes; } foreach ( $theme_json['settings']['blocks'] as $name => $node ) { $selector = null; if ( isset( $selectors[ $name ]['selector'] ) ) { $selector = $selectors[ $name ]['selector']; } $nodes[] = array( 'path' => array( 'settings', 'blocks', $name ), 'selector' => $selector, ); } return $nodes; } /** * Builds metadata for the style nodes, which returns in the form of: * * [ * [ * 'path' => [ 'path', 'to', 'some', 'node' ], * 'selector' => 'CSS selector for some node', * 'duotone' => 'CSS selector for duotone for some node' * ], * [ * 'path' => ['path', 'to', 'other', 'node' ], * 'selector' => 'CSS selector for other node', * 'duotone' => null * ], * ] * * @since 5.8.0 * * @param array $theme_json The tree to extract style nodes from. * @param array $selectors List of selectors per block. * @return array */ protected static function get_style_nodes( $theme_json, $selectors = array() ) { $nodes = array(); if ( ! isset( $theme_json['styles'] ) ) { return $nodes; } // Top-level. $nodes[] = array( 'path' => array( 'styles' ), 'selector' => static::ROOT_BLOCK_SELECTOR, ); if ( isset( $theme_json['styles']['elements'] ) ) { foreach ( $theme_json['styles']['elements'] as $element => $node ) { $nodes[] = array( 'path' => array( 'styles', 'elements', $element ), 'selector' => static::ELEMENTS[ $element ], ); } } // Blocks. if ( ! isset( $theme_json['styles']['blocks'] ) ) { return $nodes; } foreach ( $theme_json['styles']['blocks'] as $name => $node ) { $selector = null; if ( isset( $selectors[ $name ]['selector'] ) ) { $selector = $selectors[ $name ]['selector']; } $duotone_selector = null; if ( isset( $selectors[ $name ]['duotone'] ) ) { $duotone_selector = $selectors[ $name ]['duotone']; } $nodes[] = array( 'path' => array( 'styles', 'blocks', $name ), 'selector' => $selector, 'duotone' => $duotone_selector, ); if ( isset( $theme_json['styles']['blocks'][ $name ]['elements'] ) ) { foreach ( $theme_json['styles']['blocks'][ $name ]['elements'] as $element => $node ) { $nodes[] = array( 'path' => array( 'styles', 'blocks', $name, 'elements', $element ), 'selector' => $selectors[ $name ]['elements'][ $element ], ); } } } return $nodes; } /** * For metadata values that can either be booleans or paths to booleans, gets the value. * * ```php * $data = array( * 'color' => array( * 'defaultPalette' => true * ) * ); * * static::get_metadata_boolean( $data, false ); * // => false * * static::get_metadata_boolean( $data, array( 'color', 'defaultPalette' ) ); * // => true * ``` * * @since 6.0.0 * * @param array $data The data to inspect. * @param bool|array $path Boolean or path to a boolean. * @param bool $default Default value if the referenced path is missing. * Default false. * @return bool Value of boolean metadata. */ protected static function get_metadata_boolean( $data, $path, $default = false ) { if ( is_bool( $path ) ) { return $path; } if ( is_array( $path ) ) { $value = _wp_array_get( $data, $path ); if ( null !== $value ) { return $value; } } return $default; } /** * Merge new incoming data. * * @since 5.8.0 * @since 5.9.0 Duotone preset also has origins. * * @param WP_Theme_JSON $incoming Data to merge. */ public function merge( $incoming ) { $incoming_data = $incoming->get_raw_data(); $this->theme_json = array_replace_recursive( $this->theme_json, $incoming_data ); /* * The array_replace_recursive algorithm merges at the leaf level, * but we don't want leaf arrays to be merged, so we overwrite it. * * For leaf values that are sequential arrays it will use the numeric indexes for replacement. * We rather replace the existing with the incoming value, if it exists. * This is the case of spacing.units. * * For leaf values that are associative arrays it will merge them as expected. * This is also not the behavior we want for the current associative arrays (presets). * We rather replace the existing with the incoming value, if it exists. * This happens, for example, when we merge data from theme.json upon existing * theme supports or when we merge anything coming from the same source twice. * This is the case of color.palette, color.gradients, color.duotone, * typography.fontSizes, or typography.fontFamilies. * * Additionally, for some preset types, we also want to make sure the * values they introduce don't conflict with default values. We do so * by checking the incoming slugs for theme presets and compare them * with the equivalent default presets: if a slug is present as a default * we remove it from the theme presets. */ $nodes = static::get_setting_nodes( $incoming_data ); $slugs_global = static::get_default_slugs( $this->theme_json, array( 'settings' ) ); foreach ( $nodes as $node ) { $slugs_node = static::get_default_slugs( $this->theme_json, $node['path'] ); $slugs = array_merge_recursive( $slugs_global, $slugs_node ); // Replace the spacing.units. $path = array_merge( $node['path'], array( 'spacing', 'units' ) ); $content = _wp_array_get( $incoming_data, $path, null ); if ( isset( $content ) ) { _wp_array_set( $this->theme_json, $path, $content ); } // Replace the presets. foreach ( static::PRESETS_METADATA as $preset ) { $override_preset = ! static::get_metadata_boolean( $this->theme_json['settings'], $preset['prevent_override'], true ); foreach ( static::VALID_ORIGINS as $origin ) { $base_path = array_merge( $node['path'], $preset['path'] ); $path = array_merge( $base_path, array( $origin ) ); $content = _wp_array_get( $incoming_data, $path, null ); if ( ! isset( $content ) ) { continue; } if ( 'theme' === $origin && $preset['use_default_names'] ) { foreach ( $content as &$item ) { if ( ! array_key_exists( 'name', $item ) ) { $name = static::get_name_from_defaults( $item['slug'], $base_path ); if ( null !== $name ) { $item['name'] = $name; } } } } if ( ( 'theme' !== $origin ) || ( 'theme' === $origin && $override_preset ) ) { _wp_array_set( $this->theme_json, $path, $content ); } else { $slugs_for_preset = _wp_array_get( $slugs, $preset['path'], array() ); $content = static::filter_slugs( $content, $slugs_for_preset ); _wp_array_set( $this->theme_json, $path, $content ); } } } } } /** * Converts all filter (duotone) presets into SVGs. * * @since 5.9.1 * * @param array $origins List of origins to process. * @return string SVG filters. */ public function get_svg_filters( $origins ) { $blocks_metadata = static::get_blocks_metadata(); $setting_nodes = static::get_setting_nodes( $this->theme_json, $blocks_metadata ); $filters = ''; foreach ( $setting_nodes as $metadata ) { $node = _wp_array_get( $this->theme_json, $metadata['path'], array() ); if ( empty( $node['color']['duotone'] ) ) { continue; } $duotone_presets = $node['color']['duotone']; foreach ( $origins as $origin ) { if ( ! isset( $duotone_presets[ $origin ] ) ) { continue; } foreach ( $duotone_presets[ $origin ] as $duotone_preset ) { $filters .= wp_get_duotone_filter_svg( $duotone_preset ); } } } return $filters; } /** * Returns whether a presets should be overridden or not. * * @since 5.9.0 * @deprecated 6.0.0 Use {@see 'get_metadata_boolean'} instead. * * @param array $theme_json The theme.json like structure to inspect. * @param array $path Path to inspect. * @param bool|array $override Data to compute whether to override the preset. * @return boolean */ protected static function should_override_preset( $theme_json, $path, $override ) { _deprecated_function( __METHOD__, '6.0.0', 'get_metadata_boolean' ); if ( is_bool( $override ) ) { return $override; } /* * The relationship between whether to override the defaults * and whether the defaults are enabled is inverse: * * - If defaults are enabled => theme presets should not be overridden * - If defaults are disabled => theme presets should be overridden * * For example, a theme sets defaultPalette to false, * making the default palette hidden from the user. * In that case, we want all the theme presets to be present, * so they should override the defaults. */ if ( is_array( $override ) ) { $value = _wp_array_get( $theme_json, array_merge( $path, $override ) ); if ( isset( $value ) ) { return ! $value; } // Search the top-level key if none was found for this node. $value = _wp_array_get( $theme_json, array_merge( array( 'settings' ), $override ) ); if ( isset( $value ) ) { return ! $value; } return true; } } /** * Returns the default slugs for all the presets in an associative array * whose keys are the preset paths and the leafs is the list of slugs. * * For example: * * array( * 'color' => array( * 'palette' => array( 'slug-1', 'slug-2' ), * 'gradients' => array( 'slug-3', 'slug-4' ), * ), * ) * * @since 5.9.0 * * @param array $data A theme.json like structure. * @param array $node_path The path to inspect. It's 'settings' by default. * @return array */ protected static function get_default_slugs( $data, $node_path ) { $slugs = array(); foreach ( static::PRESETS_METADATA as $metadata ) { $path = array_merge( $node_path, $metadata['path'], array( 'default' ) ); $preset = _wp_array_get( $data, $path, null ); if ( ! isset( $preset ) ) { continue; } $slugs_for_preset = array(); $slugs_for_preset = array_map( static function( $value ) { return isset( $value['slug'] ) ? $value['slug'] : null; }, $preset ); _wp_array_set( $slugs, $metadata['path'], $slugs_for_preset ); } return $slugs; } /** * Get a `default`'s preset name by a provided slug. * * @since 5.9.0 * * @param string $slug The slug we want to find a match from default presets. * @param array $base_path The path to inspect. It's 'settings' by default. * @return string|null */ protected function get_name_from_defaults( $slug, $base_path ) { $path = array_merge( $base_path, array( 'default' ) ); $default_content = _wp_array_get( $this->theme_json, $path, null ); if ( ! $default_content ) { return null; } foreach ( $default_content as $item ) { if ( $slug === $item['slug'] ) { return $item['name']; } } return null; } /** * Removes the preset values whose slug is equal to any of given slugs. * * @since 5.9.0 * * @param array $node The node with the presets to validate. * @param array $slugs The slugs that should not be overridden. * @return array The new node. */ protected static function filter_slugs( $node, $slugs ) { if ( empty( $slugs ) ) { return $node; } $new_node = array(); foreach ( $node as $value ) { if ( isset( $value['slug'] ) && ! in_array( $value['slug'], $slugs, true ) ) { $new_node[] = $value; } } return $new_node; } /** * Removes insecure data from theme.json. * * @since 5.9.0 * * @param array $theme_json Structure to sanitize. * @return array Sanitized structure. */ public static function remove_insecure_properties( $theme_json ) { $sanitized = array(); $theme_json = WP_Theme_JSON_Schema::migrate( $theme_json ); $valid_block_names = array_keys( static::get_blocks_metadata() ); $valid_element_names = array_keys( static::ELEMENTS ); $theme_json = static::sanitize( $theme_json, $valid_block_names, $valid_element_names ); $blocks_metadata = static::get_blocks_metadata(); $style_nodes = static::get_style_nodes( $theme_json, $blocks_metadata ); foreach ( $style_nodes as $metadata ) { $input = _wp_array_get( $theme_json, $metadata['path'], array() ); if ( empty( $input ) ) { continue; } $output = static::remove_insecure_styles( $input ); if ( ! empty( $output ) ) { _wp_array_set( $sanitized, $metadata['path'], $output ); } } $setting_nodes = static::get_setting_nodes( $theme_json ); foreach ( $setting_nodes as $metadata ) { $input = _wp_array_get( $theme_json, $metadata['path'], array() ); if ( empty( $input ) ) { continue; } $output = static::remove_insecure_settings( $input ); if ( ! empty( $output ) ) { _wp_array_set( $sanitized, $metadata['path'], $output ); } } if ( empty( $sanitized['styles'] ) ) { unset( $theme_json['styles'] ); } else { $theme_json['styles'] = $sanitized['styles']; } if ( empty( $sanitized['settings'] ) ) { unset( $theme_json['settings'] ); } else { $theme_json['settings'] = $sanitized['settings']; } return $theme_json; } /** * Processes a setting node and returns the same node * without the insecure settings. * * @since 5.9.0 * * @param array $input Node to process. * @return array */ protected static function remove_insecure_settings( $input ) { $output = array(); foreach ( static::PRESETS_METADATA as $preset_metadata ) { foreach ( static::VALID_ORIGINS as $origin ) { $path_with_origin = array_merge( $preset_metadata['path'], array( $origin ) ); $presets = _wp_array_get( $input, $path_with_origin, null ); if ( null === $presets ) { continue; } $escaped_preset = array(); foreach ( $presets as $preset ) { if ( esc_attr( esc_html( $preset['name'] ) ) === $preset['name'] && sanitize_html_class( $preset['slug'] ) === $preset['slug'] ) { $value = null; if ( isset( $preset_metadata['value_key'], $preset[ $preset_metadata['value_key'] ] ) ) { $value = $preset[ $preset_metadata['value_key'] ]; } elseif ( isset( $preset_metadata['value_func'] ) && is_callable( $preset_metadata['value_func'] ) ) { $value = call_user_func( $preset_metadata['value_func'], $preset ); } $preset_is_valid = true; foreach ( $preset_metadata['properties'] as $property ) { if ( ! static::is_safe_css_declaration( $property, $value ) ) { $preset_is_valid = false; break; } } if ( $preset_is_valid ) { $escaped_preset[] = $preset; } } } if ( ! empty( $escaped_preset ) ) { _wp_array_set( $output, $path_with_origin, $escaped_preset ); } } } return $output; } /** * Processes a style node and returns the same node * without the insecure styles. * * @since 5.9.0 * * @param array $input Node to process. * @return array */ protected static function remove_insecure_styles( $input ) { $output = array(); $declarations = static::compute_style_properties( $input ); foreach ( $declarations as $declaration ) { if ( static::is_safe_css_declaration( $declaration['name'], $declaration['value'] ) ) { $path = static::PROPERTIES_METADATA[ $declaration['name'] ]; // Check the value isn't an array before adding so as to not // double up shorthand and longhand styles. $value = _wp_array_get( $input, $path, array() ); if ( ! is_array( $value ) ) { _wp_array_set( $output, $path, $value ); } } } return $output; } /** * Checks that a declaration provided by the user is safe. * * @since 5.9.0 * * @param string $property_name Property name in a CSS declaration, i.e. the `color` in `color: red`. * @param string $property_value Value in a CSS declaration, i.e. the `red` in `color: red`. * @return bool */ protected static function is_safe_css_declaration( $property_name, $property_value ) { $style_to_validate = $property_name . ': ' . $property_value; $filtered = esc_html( safecss_filter_attr( $style_to_validate ) ); return ! empty( trim( $filtered ) ); } /** * Returns the raw data. * * @since 5.8.0 * * @return array Raw data. */ public function get_raw_data() { return $this->theme_json; } /** * Transforms the given editor settings according the * add_theme_support format to the theme.json format. * * @since 5.8.0 * * @param array $settings Existing editor settings. * @return array Config that adheres to the theme.json schema. */ public static function get_from_editor_settings( $settings ) { $theme_settings = array( 'version' => static::LATEST_SCHEMA, 'settings' => array(), ); // Deprecated theme supports. if ( isset( $settings['disableCustomColors'] ) ) { if ( ! isset( $theme_settings['settings']['color'] ) ) { $theme_settings['settings']['color'] = array(); } $theme_settings['settings']['color']['custom'] = ! $settings['disableCustomColors']; } if ( isset( $settings['disableCustomGradients'] ) ) { if ( ! isset( $theme_settings['settings']['color'] ) ) { $theme_settings['settings']['color'] = array(); } $theme_settings['settings']['color']['customGradient'] = ! $settings['disableCustomGradients']; } if ( isset( $settings['disableCustomFontSizes'] ) ) { if ( ! isset( $theme_settings['settings']['typography'] ) ) { $theme_settings['settings']['typography'] = array(); } $theme_settings['settings']['typography']['customFontSize'] = ! $settings['disableCustomFontSizes']; } if ( isset( $settings['enableCustomLineHeight'] ) ) { if ( ! isset( $theme_settings['settings']['typography'] ) ) { $theme_settings['settings']['typography'] = array(); } $theme_settings['settings']['typography']['lineHeight'] = $settings['enableCustomLineHeight']; } if ( isset( $settings['enableCustomUnits'] ) ) { if ( ! isset( $theme_settings['settings']['spacing'] ) ) { $theme_settings['settings']['spacing'] = array(); } $theme_settings['settings']['spacing']['units'] = ( true === $settings['enableCustomUnits'] ) ? array( 'px', 'em', 'rem', 'vh', 'vw', '%' ) : $settings['enableCustomUnits']; } if ( isset( $settings['colors'] ) ) { if ( ! isset( $theme_settings['settings']['color'] ) ) { $theme_settings['settings']['color'] = array(); } $theme_settings['settings']['color']['palette'] = $settings['colors']; } if ( isset( $settings['gradients'] ) ) { if ( ! isset( $theme_settings['settings']['color'] ) ) { $theme_settings['settings']['color'] = array(); } $theme_settings['settings']['color']['gradients'] = $settings['gradients']; } if ( isset( $settings['fontSizes'] ) ) { $font_sizes = $settings['fontSizes']; // Back-compatibility for presets without units. foreach ( $font_sizes as $key => $font_size ) { if ( is_numeric( $font_size['size'] ) ) { $font_sizes[ $key ]['size'] = $font_size['size'] . 'px'; } } if ( ! isset( $theme_settings['settings']['typography'] ) ) { $theme_settings['settings']['typography'] = array(); } $theme_settings['settings']['typography']['fontSizes'] = $font_sizes; } if ( isset( $settings['enableCustomSpacing'] ) ) { if ( ! isset( $theme_settings['settings']['spacing'] ) ) { $theme_settings['settings']['spacing'] = array(); } $theme_settings['settings']['spacing']['padding'] = $settings['enableCustomSpacing']; } return $theme_settings; } /** * Returns the current theme's wanted patterns(slugs) to be * registered from Pattern Directory. * * @since 6.0.0 * * @return string[] */ public function get_patterns() { if ( isset( $this->theme_json['patterns'] ) && is_array( $this->theme_json['patterns'] ) ) { return $this->theme_json['patterns']; } return array(); } /** * Returns a valid theme.json as provided by a theme. * * Unlike get_raw_data() this returns the presets flattened, as provided by a theme. * This also uses appearanceTools instead of their opt-ins if all of them are true. * * @since 6.0.0 * * @return array */ public function get_data() { $output = $this->theme_json; $nodes = static::get_setting_nodes( $output ); /** * Flatten the theme & custom origins into a single one. * * For example, the following: * * { * "settings": { * "color": { * "palette": { * "theme": [ {} ], * "custom": [ {} ] * } * } * } * } * * will be converted to: * * { * "settings": { * "color": { * "palette": [ {} ] * } * } * } */ foreach ( $nodes as $node ) { foreach ( static::PRESETS_METADATA as $preset_metadata ) { $path = array_merge( $node['path'], $preset_metadata['path'] ); $preset = _wp_array_get( $output, $path, null ); if ( null === $preset ) { continue; } $items = array(); if ( isset( $preset['theme'] ) ) { foreach ( $preset['theme'] as $item ) { $slug = $item['slug']; unset( $item['slug'] ); $items[ $slug ] = $item; } } if ( isset( $preset['custom'] ) ) { foreach ( $preset['custom'] as $item ) { $slug = $item['slug']; unset( $item['slug'] ); $items[ $slug ] = $item; } } $flattened_preset = array(); foreach ( $items as $slug => $value ) { $flattened_preset[] = array_merge( array( 'slug' => $slug ), $value ); } _wp_array_set( $output, $path, $flattened_preset ); } } // If all of the static::APPEARANCE_TOOLS_OPT_INS are true, // this code unsets them and sets 'appearanceTools' instead. foreach ( $nodes as $node ) { $all_opt_ins_are_set = true; foreach ( static::APPEARANCE_TOOLS_OPT_INS as $opt_in_path ) { $full_path = array_merge( $node['path'], $opt_in_path ); // Use "unset prop" as a marker instead of "null" because // "null" can be a valid value for some props (e.g. blockGap). $opt_in_value = _wp_array_get( $output, $full_path, 'unset prop' ); if ( 'unset prop' === $opt_in_value ) { $all_opt_ins_are_set = false; break; } } if ( $all_opt_ins_are_set ) { _wp_array_set( $output, array_merge( $node['path'], array( 'appearanceTools' ) ), true ); foreach ( static::APPEARANCE_TOOLS_OPT_INS as $opt_in_path ) { $full_path = array_merge( $node['path'], $opt_in_path ); // Use "unset prop" as a marker instead of "null" because // "null" can be a valid value for some props (e.g. blockGap). $opt_in_value = _wp_array_get( $output, $full_path, 'unset prop' ); if ( true !== $opt_in_value ) { continue; } // The following could be improved to be path independent. // At the moment it relies on a couple of assumptions: // // - all opt-ins having a path of size 2. // - there's two sources of settings: the top-level and the block-level. if ( ( 1 === count( $node['path'] ) ) && ( 'settings' === $node['path'][0] ) ) { // Top-level settings. unset( $output['settings'][ $opt_in_path[0] ][ $opt_in_path[1] ] ); if ( empty( $output['settings'][ $opt_in_path[0] ] ) ) { unset( $output['settings'][ $opt_in_path[0] ] ); } } elseif ( ( 3 === count( $node['path'] ) ) && ( 'settings' === $node['path'][0] ) && ( 'blocks' === $node['path'][1] ) ) { // Block-level settings. $block_name = $node['path'][2]; unset( $output['settings']['blocks'][ $block_name ][ $opt_in_path[0] ][ $opt_in_path[1] ] ); if ( empty( $output['settings']['blocks'][ $block_name ][ $opt_in_path[0] ] ) ) { unset( $output['settings']['blocks'][ $block_name ][ $opt_in_path[0] ] ); } } } } } wp_recursive_ksort( $output ); return $output; } } $post = get_post( $attachment_id ); if ( ! $post ) { return false; } if ( 'attachment' !== $post->post_type ) { return false; } $url = ''; // Get attached file. $file = get_post_meta( $post->ID, '_wp_attached_file', true ); if ( $file ) { // Get upload directory. $uploads = wp_get_upload_dir(); if ( $uploads && false === $uploads['error'] ) { // Check that the upload base exists in the file location. if ( 0 === strpos( $file, $uploads['basedir'] ) ) { // Replace file location with url location. $url = str_replace( $uploads['basedir'], $uploads['baseurl'], $file ); } elseif ( false !== strpos( $file, 'wp-content/uploads' ) ) { // Get the directory name relative to the basedir (back compat for pre-2.7 uploads). $url = trailingslashit( $uploads['baseurl'] . '/' . _wp_get_attachment_relative_path( $file ) ) . wp_basename( $file ); } else { // It's a newly-uploaded file, therefore $file is relative to the basedir. $url = $uploads['baseurl'] . "/$file"; } } } /* * If any of the above options failed, Fallback on the GUID as used pre-2.7, * not recommended to rely upon this. */ if ( ! $url ) { $url = get_the_guid( $post->ID ); } // On SSL front end, URLs should be HTTPS. if ( is_ssl() && ! is_admin() && 'wp-login.php' !== $pagenow ) { $url = set_url_scheme( $url ); } /** * Filters the attachment URL. * * @since 2.1.0 * * @param string $url URL for the given attachment. * @param int $attachment_id Attachment post ID. */ $url = apply_filters( 'wp_get_attachment_url', $url, $post->ID ); if ( ! $url ) { return false; } return $url; } /** * Retrieves the caption for an attachment. * * @since 4.6.0 * * @param int $post_id Optional. Attachment ID. Default is the ID of the global `$post`. * @return string|false Attachment caption on success, false on failure. */ function wp_get_attachment_caption( $post_id = 0 ) { $post_id = (int) $post_id; $post = get_post( $post_id ); if ( ! $post ) { return false; } if ( 'attachment' !== $post->post_type ) { return false; } $caption = $post->post_excerpt; /** * Filters the attachment caption. * * @since 4.6.0 * * @param string $caption Caption for the given attachment. * @param int $post_id Attachment ID. */ return apply_filters( 'wp_get_attachment_caption', $caption, $post->ID ); } /** * Retrieve thumbnail for an attachment. * * @since 2.1.0 * * @param int $post_id Optional. Attachment ID. Default is the ID of the global `$post`. * @return string|false Thumbnail file path on success, false on failure. */ function wp_get_attachment_thumb_file( $post_id = 0 ) { $post_id = (int) $post_id; $post = get_post( $post_id ); if ( ! $post ) { return false; } $imagedata = wp_get_attachment_metadata( $post->ID ); if ( ! is_array( $imagedata ) ) { return false; } $file = get_attached_file( $post->ID ); if ( ! empty( $imagedata['thumb'] ) ) { $thumbfile = str_replace( wp_basename( $file ), $imagedata['thumb'], $file ); if ( file_exists( $thumbfile ) ) { /** * Filters the attachment thumbnail file path. * * @since 2.1.0 * * @param string $thumbfile File path to the attachment thumbnail. * @param int $post_id Attachment ID. */ return apply_filters( 'wp_get_attachment_thumb_file', $thumbfile, $post->ID ); } } return false; } /** * Retrieve URL for an attachment thumbnail. * * @since 2.1.0 * * @param int $post_id Optional. Attachment ID. Default is the ID of the global `$post`. * @return string|false Thumbnail URL on success, false on failure. */ function wp_get_attachment_thumb_url( $post_id = 0 ) { $post_id = (int) $post_id; $post = get_post( $post_id ); if ( ! $post ) { return false; } $url = wp_get_attachment_url( $post->ID ); if ( ! $url ) { return false; } $sized = image_downsize( $post_id, 'thumbnail' ); if ( $sized ) { return $sized[0]; } $thumb = wp_get_attachment_thumb_file( $post->ID ); if ( ! $thumb ) { return false; } $url = str_replace( wp_basename( $url ), wp_basename( $thumb ), $url ); /** * Filters the attachment thumbnail URL. * * @since 2.1.0 * * @param string $url URL for the attachment thumbnail. * @param int $post_id Attachment ID. */ return apply_filters( 'wp_get_attachment_thumb_url', $url, $post->ID ); } /** * Verifies an attachment is of a given type. * * @since 4.2.0 * * @param string $type Attachment type. Accepts 'image', 'audio', or 'video'. * @param int|WP_Post $post Optional. Attachment ID or object. Default is global $post. * @return bool True if one of the accepted types, false otherwise. */ function wp_attachment_is( $type, $post = null ) { $post = get_post( $post ); if ( ! $post ) { return false; } $file = get_attached_file( $post->ID ); if ( ! $file ) { return false; } if ( 0 === strpos( $post->post_mime_type, $type . '/' ) ) { return true; } $check = wp_check_filetype( $file ); if ( empty( $check['ext'] ) ) { return false; } $ext = $check['ext']; if ( 'import' !== $post->post_mime_type ) { return $type === $ext; } switch ( $type ) { case 'image': $image_exts = array( 'jpg', 'jpeg', 'jpe', 'gif', 'png', 'webp' ); return in_array( $ext, $image_exts, true ); case 'audio': return in_array( $ext, wp_get_audio_extensions(), true ); case 'video': return in_array( $ext, wp_get_video_extensions(), true ); default: return $type === $ext; } } /** * Determines whether an attachment is an image. * * For more information on this and similar theme functions, check out * the {@link https://developer.wordpress.org/themes/basics/conditional-tags/ * Conditional Tags} article in the Theme Developer Handbook. * * @since 2.1.0 * @since 4.2.0 Modified into wrapper for wp_attachment_is() and * allowed WP_Post object to be passed. * * @param int|WP_Post $post Optional. Attachment ID or object. Default is global $post. * @return bool Whether the attachment is an image. */ function wp_attachment_is_image( $post = null ) { return wp_attachment_is( 'image', $post ); } /** * Retrieve the icon for a MIME type or attachment. * * @since 2.1.0 * * @param string|int $mime MIME type or attachment ID. * @return string|false Icon, false otherwise. */ function wp_mime_type_icon( $mime = 0 ) { if ( ! is_numeric( $mime ) ) { $icon = wp_cache_get( "mime_type_icon_$mime" ); } $post_id = 0; if ( empty( $icon ) ) { $post_mimes = array(); if ( is_numeric( $mime ) ) { $mime = (int) $mime; $post = get_post( $mime ); if ( $post ) { $post_id = (int) $post->ID; $file = get_attached_file( $post_id ); $ext = preg_replace( '/^.+?\.([^.]+)$/', '$1', $file ); if ( ! empty( $ext ) ) { $post_mimes[] = $ext; $ext_type = wp_ext2type( $ext ); if ( $ext_type ) { $post_mimes[] = $ext_type; } } $mime = $post->post_mime_type; } else { $mime = 0; } } else { $post_mimes[] = $mime; } $icon_files = wp_cache_get( 'icon_files' ); if ( ! is_array( $icon_files ) ) { /** * Filters the icon directory path. * * @since 2.0.0 * * @param string $path Icon directory absolute path. */ $icon_dir = apply_filters( 'icon_dir', ABSPATH . WPINC . '/images/media' ); /** * Filters the icon directory URI. * * @since 2.0.0 * * @param string $uri Icon directory URI. */ $icon_dir_uri = apply_filters( 'icon_dir_uri', includes_url( 'images/media' ) ); /** * Filters the array of icon directory URIs. * * @since 2.5.0 * * @param string[] $uris Array of icon directory URIs keyed by directory absolute path. */ $dirs = apply_filters( 'icon_dirs', array( $icon_dir => $icon_dir_uri ) ); $icon_files = array(); while ( $dirs ) { $keys = array_keys( $dirs ); $dir = array_shift( $keys ); $uri = array_shift( $dirs ); $dh = opendir( $dir ); if ( $dh ) { while ( false !== $file = readdir( $dh ) ) { $file = wp_basename( $file ); if ( '.' === substr( $file, 0, 1 ) ) { continue; } $ext = strtolower( substr( $file, -4 ) ); if ( ! in_array( $ext, array( '.png', '.gif', '.jpg' ), true ) ) { if ( is_dir( "$dir/$file" ) ) { $dirs[ "$dir/$file" ] = "$uri/$file"; } continue; } $icon_files[ "$dir/$file" ] = "$uri/$file"; } closedir( $dh ); } } wp_cache_add( 'icon_files', $icon_files, 'default', 600 ); } $types = array(); // Icon wp_basename - extension = MIME wildcard. foreach ( $icon_files as $file => $uri ) { $types[ preg_replace( '/^([^.]*).*$/', '$1', wp_basename( $file ) ) ] =& $icon_files[ $file ]; } if ( ! empty( $mime ) ) { $post_mimes[] = substr( $mime, 0, strpos( $mime, '/' ) ); $post_mimes[] = substr( $mime, strpos( $mime, '/' ) + 1 ); $post_mimes[] = str_replace( '/', '_', $mime ); } $matches = wp_match_mime_types( array_keys( $types ), $post_mimes ); $matches['default'] = array( 'default' ); foreach ( $matches as $match => $wilds ) { foreach ( $wilds as $wild ) { if ( ! isset( $types[ $wild ] ) ) { continue; } $icon = $types[ $wild ]; if ( ! is_numeric( $mime ) ) { wp_cache_add( "mime_type_icon_$mime", $icon ); } break 2; } } } /** * Filters the mime type icon. * * @since 2.1.0 * * @param string $icon Path to the mime type icon. * @param string $mime Mime type. * @param int $post_id Attachment ID. Will equal 0 if the function passed * the mime type. */ return apply_filters( 'wp_mime_type_icon', $icon, $mime, $post_id ); } /** * Check for changed slugs for published post objects and save the old slug. * * The function is used when a post object of any type is updated, * by comparing the current and previous post objects. * * If the slug was changed and not already part of the old slugs then it will be * added to the post meta field ('_wp_old_slug') for storing old slugs for that * post. * * The most logically usage of this function is redirecting changed post objects, so * that those that linked to an changed post will be redirected to the new post. * * @since 2.1.0 * * @param int $post_id Post ID. * @param WP_Post $post The Post Object * @param WP_Post $post_before The Previous Post Object */ function wp_check_for_changed_slugs( $post_id, $post, $post_before ) { // Don't bother if it hasn't changed. if ( $post->post_name == $post_before->post_name ) { return; } // We're only concerned with published, non-hierarchical objects. if ( ! ( 'publish' === $post->post_status || ( 'attachment' === get_post_type( $post ) && 'inherit' === $post->post_status ) ) || is_post_type_hierarchical( $post->post_type ) ) { return; } $old_slugs = (array) get_post_meta( $post_id, '_wp_old_slug' ); // If we haven't added this old slug before, add it now. if ( ! empty( $post_before->post_name ) && ! in_array( $post_before->post_name, $old_slugs, true ) ) { add_post_meta( $post_id, '_wp_old_slug', $post_before->post_name ); } // If the new slug was used previously, delete it from the list. if ( in_array( $post->post_name, $old_slugs, true ) ) { delete_post_meta( $post_id, '_wp_old_slug', $post->post_name ); } } /** * Check for changed dates for published post objects and save the old date. * * The function is used when a post object of any type is updated, * by comparing the current and previous post objects. * * If the date was changed and not already part of the old dates then it will be * added to the post meta field ('_wp_old_date') for storing old dates for that * post. * * The most logically usage of this function is redirecting changed post objects, so * that those that linked to an changed post will be redirected to the new post. * * @since 4.9.3 * * @param int $post_id Post ID. * @param WP_Post $post The Post Object * @param WP_Post $post_before The Previous Post Object */ function wp_check_for_changed_dates( $post_id, $post, $post_before ) { $previous_date = gmdate( 'Y-m-d', strtotime( $post_before->post_date ) ); $new_date = gmdate( 'Y-m-d', strtotime( $post->post_date ) ); // Don't bother if it hasn't changed. if ( $new_date == $previous_date ) { return; } // We're only concerned with published, non-hierarchical objects. if ( ! ( 'publish' === $post->post_status || ( 'attachment' === get_post_type( $post ) && 'inherit' === $post->post_status ) ) || is_post_type_hierarchical( $post->post_type ) ) { return; } $old_dates = (array) get_post_meta( $post_id, '_wp_old_date' ); // If we haven't added this old date before, add it now. if ( ! empty( $previous_date ) && ! in_array( $previous_date, $old_dates, true ) ) { add_post_meta( $post_id, '_wp_old_date', $previous_date ); } // If the new slug was used previously, delete it from the list. if ( in_array( $new_date, $old_dates, true ) ) { delete_post_meta( $post_id, '_wp_old_date', $new_date ); } } /** * Retrieve the private post SQL based on capability. * * This function provides a standardized way to appropriately select on the * post_status of a post type. The function will return a piece of SQL code * that can be added to a WHERE clause; this SQL is constructed to allow all * published posts, and all private posts to which the user has access. * * @since 2.2.0 * @since 4.3.0 Added the ability to pass an array to `$post_type`. * * @param string|array $post_type Single post type or an array of post types. Currently only supports 'post' or 'page'. * @return string SQL code that can be added to a where clause. */ function get_private_posts_cap_sql( $post_type ) { return get_posts_by_author_sql( $post_type, false ); } /** * Retrieve the post SQL based on capability, author, and type. * * @since 3.0.0 * @since 4.3.0 Introduced the ability to pass an array of post types to `$post_type`. * * @see get_private_posts_cap_sql() * @global wpdb $wpdb WordPress database abstraction object. * * @param string|string[] $post_type Single post type or an array of post types. * @param bool $full Optional. Returns a full WHERE statement instead of just * an 'andalso' term. Default true. * @param int $post_author Optional. Query posts having a single author ID. Default null. * @param bool $public_only Optional. Only return public posts. Skips cap checks for * $current_user. Default false. * @return string SQL WHERE code that can be added to a query. */ function get_posts_by_author_sql( $post_type, $full = true, $post_author = null, $public_only = false ) { global $wpdb; if ( is_array( $post_type ) ) { $post_types = $post_type; } else { $post_types = array( $post_type ); } $post_type_clauses = array(); foreach ( $post_types as $post_type ) { $post_type_obj = get_post_type_object( $post_type ); if ( ! $post_type_obj ) { continue; } /** * Filters the capability to read private posts for a custom post type * when generating SQL for getting posts by author. * * @since 2.2.0 * @deprecated 3.2.0 The hook transitioned from "somewhat useless" to "totally useless". * * @param string $cap Capability. */ $cap = apply_filters_deprecated( 'pub_priv_sql_capability', array( '' ), '3.2.0' ); if ( ! $cap ) { $cap = current_user_can( $post_type_obj->cap->read_private_posts ); } // Only need to check the cap if $public_only is false. $post_status_sql = "post_status = 'publish'"; if ( false === $public_only ) { if ( $cap ) { // Does the user have the capability to view private posts? Guess so. $post_status_sql .= " OR post_status = 'private'"; } elseif ( is_user_logged_in() ) { // Users can view their own private posts. $id = get_current_user_id(); if ( null === $post_author || ! $full ) { $post_status_sql .= " OR post_status = 'private' AND post_author = $id"; } elseif ( $id == (int) $post_author ) { $post_status_sql .= " OR post_status = 'private'"; } // Else none. } // Else none. } $post_type_clauses[] = "( post_type = '" . $post_type . "' AND ( $post_status_sql ) )"; } if ( empty( $post_type_clauses ) ) { return $full ? 'WHERE 1 = 0' : '1 = 0'; } $sql = '( ' . implode( ' OR ', $post_type_clauses ) . ' )'; if ( null !== $post_author ) { $sql .= $wpdb->prepare( ' AND post_author = %d', $post_author ); } if ( $full ) { $sql = 'WHERE ' . $sql; } return $sql; } /** * Retrieves the most recent time that a post on the site was published. * * The server timezone is the default and is the difference between GMT and * server time. The 'blog' value is the date when the last post was posted. * The 'gmt' is when the last post was posted in GMT formatted date. * * @since 0.71 * @since 4.4.0 The `$post_type` argument was added. * * @param string $timezone Optional. The timezone for the timestamp. Accepts 'server', 'blog', or 'gmt'. * 'server' uses the server's internal timezone. * 'blog' uses the `post_date` field, which proxies to the timezone set for the site. * 'gmt' uses the `post_date_gmt` field. * Default 'server'. * @param string $post_type Optional. The post type to check. Default 'any'. * @return string The date of the last post, or false on failure. */ function get_lastpostdate( $timezone = 'server', $post_type = 'any' ) { $lastpostdate = _get_last_post_time( $timezone, 'date', $post_type ); /** * Filters the most recent time that a post on the site was published. * * @since 2.3.0 * @since 5.5.0 Added the `$post_type` parameter. * * @param string|false $lastpostdate The most recent time that a post was published, * in 'Y-m-d H:i:s' format. False on failure. * @param string $timezone Location to use for getting the post published date. * See get_lastpostdate() for accepted `$timezone` values. * @param string $post_type The post type to check. */ return apply_filters( 'get_lastpostdate', $lastpostdate, $timezone, $post_type ); } /** * Get the most recent time that a post on the site was modified. * * The server timezone is the default and is the difference between GMT and * server time. The 'blog' value is just when the last post was modified. * The 'gmt' is when the last post was modified in GMT time. * * @since 1.2.0 * @since 4.4.0 The `$post_type` argument was added. * * @param string $timezone Optional. The timezone for the timestamp. See get_lastpostdate() * for information on accepted values. * Default 'server'. * @param string $post_type Optional. The post type to check. Default 'any'. * @return string The timestamp in 'Y-m-d H:i:s' format, or false on failure. */ function get_lastpostmodified( $timezone = 'server', $post_type = 'any' ) { /** * Pre-filter the return value of get_lastpostmodified() before the query is run. * * @since 4.4.0 * * @param string|false $lastpostmodified The most recent time that a post was modified, * in 'Y-m-d H:i:s' format, or false. Returning anything * other than false will short-circuit the function. * @param string $timezone Location to use for getting the post modified date. * See get_lastpostdate() for accepted `$timezone` values. * @param string $post_type The post type to check. */ $lastpostmodified = apply_filters( 'pre_get_lastpostmodified', false, $timezone, $post_type ); if ( false !== $lastpostmodified ) { return $lastpostmodified; } $lastpostmodified = _get_last_post_time( $timezone, 'modified', $post_type ); $lastpostdate = get_lastpostdate( $timezone, $post_type ); if ( $lastpostdate > $lastpostmodified ) { $lastpostmodified = $lastpostdate; } /** * Filters the most recent time that a post on the site was modified. * * @since 2.3.0 * @since 5.5.0 Added the `$post_type` parameter. * * @param string|false $lastpostmodified The most recent time that a post was modified, * in 'Y-m-d H:i:s' format. False on failure. * @param string $timezone Location to use for getting the post modified date. * See get_lastpostdate() for accepted `$timezone` values. * @param string $post_type The post type to check. */ return apply_filters( 'get_lastpostmodified', $lastpostmodified, $timezone, $post_type ); } /** * Gets the timestamp of the last time any post was modified or published. * * @since 3.1.0 * @since 4.4.0 The `$post_type` argument was added. * @access private * * @global wpdb $wpdb WordPress database abstraction object. * * @param string $timezone The timezone for the timestamp. See get_lastpostdate(). * for information on accepted values. * @param string $field Post field to check. Accepts 'date' or 'modified'. * @param string $post_type Optional. The post type to check. Default 'any'. * @return string|false The timestamp in 'Y-m-d H:i:s' format, or false on failure. */ function _get_last_post_time( $timezone, $field, $post_type = 'any' ) { global $wpdb; if ( ! in_array( $field, array( 'date', 'modified' ), true ) ) { return false; } $timezone = strtolower( $timezone ); $key = "lastpost{$field}:$timezone"; if ( 'any' !== $post_type ) { $key .= ':' . sanitize_key( $post_type ); } $date = wp_cache_get( $key, 'timeinfo' ); if ( false !== $date ) { return $date; } if ( 'any' === $post_type ) { $post_types = get_post_types( array( 'public' => true ) ); array_walk( $post_types, array( $wpdb, 'escape_by_ref' ) ); $post_types = "'" . implode( "', '", $post_types ) . "'"; } else { $post_types = "'" . sanitize_key( $post_type ) . "'"; } switch ( $timezone ) { case 'gmt': $date = $wpdb->get_var( "SELECT post_{$field}_gmt FROM $wpdb->posts WHERE post_status = 'publish' AND post_type IN ({$post_types}) ORDER BY post_{$field}_gmt DESC LIMIT 1" ); break; case 'blog': $date = $wpdb->get_var( "SELECT post_{$field} FROM $wpdb->posts WHERE post_status = 'publish' AND post_type IN ({$post_types}) ORDER BY post_{$field}_gmt DESC LIMIT 1" ); break; case 'server': $add_seconds_server = gmdate( 'Z' ); $date = $wpdb->get_var( "SELECT DATE_ADD(post_{$field}_gmt, INTERVAL '$add_seconds_server' SECOND) FROM $wpdb->posts WHERE post_status = 'publish' AND post_type IN ({$post_types}) ORDER BY post_{$field}_gmt DESC LIMIT 1" ); break; } if ( $date ) { wp_cache_set( $key, $date, 'timeinfo' ); return $date; } return false; } /** * Updates posts in cache. * * @since 1.5.1 * * @param WP_Post[] $posts Array of post objects (passed by reference). */ function update_post_cache( &$posts ) { if ( ! $posts ) { return; } $data = array(); foreach ( $posts as $post ) { if ( empty( $post->filter ) || 'raw' !== $post->filter ) { $post = sanitize_post( $post, 'raw' ); } $data[ $post->ID ] = $post; } wp_cache_add_multiple( $data, 'posts' ); } /** * Will clean the post in the cache. * * Cleaning means delete from the cache of the post. Will call to clean the term * object cache associated with the post ID. * * This function not run if $_wp_suspend_cache_invalidation is not empty. See * wp_suspend_cache_invalidation(). * * @since 2.0.0 * * @global bool $_wp_suspend_cache_invalidation * * @param int|WP_Post $post Post ID or post object to remove from the cache. */ function clean_post_cache( $post ) { global $_wp_suspend_cache_invalidation; if ( ! empty( $_wp_suspend_cache_invalidation ) ) { return; } $post = get_post( $post ); if ( ! $post ) { return; } wp_cache_delete( $post->ID, 'posts' ); wp_cache_delete( $post->ID, 'post_meta' ); clean_object_term_cache( $post->ID, $post->post_type ); wp_cache_delete( 'wp_get_archives', 'general' ); /** * Fires immediately after the given post's cache is cleaned. * * @since 2.5.0 * * @param int $post_id Post ID. * @param WP_Post $post Post object. */ do_action( 'clean_post_cache', $post->ID, $post ); if ( 'page' === $post->post_type ) { wp_cache_delete( 'all_page_ids', 'posts' ); /** * Fires immediately after the given page's cache is cleaned. * * @since 2.5.0 * * @param int $post_id Post ID. */ do_action( 'clean_page_cache', $post->ID ); } wp_cache_set( 'last_changed', microtime(), 'posts' ); } /** * Call major cache updating functions for list of Post objects. * * @since 1.5.0 * * @param WP_Post[] $posts Array of Post objects * @param string $post_type Optional. Post type. Default 'post'. * @param bool $update_term_cache Optional. Whether to update the term cache. Default true. * @param bool $update_meta_cache Optional. Whether to update the meta cache. Default true. */ function update_post_caches( &$posts, $post_type = 'post', $update_term_cache = true, $update_meta_cache = true ) { // No point in doing all this work if we didn't match any posts. if ( ! $posts ) { return; } update_post_cache( $posts ); $post_ids = array(); foreach ( $posts as $post ) { $post_ids[] = $post->ID; } if ( ! $post_type ) { $post_type = 'any'; } if ( $update_term_cache ) { if ( is_array( $post_type ) ) { $ptypes = $post_type; } elseif ( 'any' === $post_type ) { $ptypes = array(); // Just use the post_types in the supplied posts. foreach ( $posts as $post ) { $ptypes[] = $post->post_type; } $ptypes = array_unique( $ptypes ); } else { $ptypes = array( $post_type ); } if ( ! empty( $ptypes ) ) { update_object_term_cache( $post_ids, $ptypes ); } } if ( $update_meta_cache ) { update_postmeta_cache( $post_ids ); } } /** * Updates metadata cache for list of post IDs. * * Performs SQL query to retrieve the metadata for the post IDs and updates the * metadata cache for the posts. Therefore, the functions, which call this * function, do not need to perform SQL queries on their own. * * @since 2.1.0 * * @param int[] $post_ids Array of post IDs. * @return array|false An array of metadata on success, false if there is nothing to update. */ function update_postmeta_cache( $post_ids ) { return update_meta_cache( 'post', $post_ids ); } /** * Will clean the attachment in the cache. * * Cleaning means delete from the cache. Optionally will clean the term * object cache associated with the attachment ID. * * This function will not run if $_wp_suspend_cache_invalidation is not empty. * * @since 3.0.0 * * @global bool $_wp_suspend_cache_invalidation * * @param int $id The attachment ID in the cache to clean. * @param bool $clean_terms Optional. Whether to clean terms cache. Default false. */ function clean_attachment_cache( $id, $clean_terms = false ) { global $_wp_suspend_cache_invalidation; if ( ! empty( $_wp_suspend_cache_invalidation ) ) { return; } $id = (int) $id; wp_cache_delete( $id, 'posts' ); wp_cache_delete( $id, 'post_meta' ); if ( $clean_terms ) { clean_object_term_cache( $id, 'attachment' ); } /** * Fires after the given attachment's cache is cleaned. * * @since 3.0.0 * * @param int $id Attachment ID. */ do_action( 'clean_attachment_cache', $id ); } // // Hooks. // /** * Hook for managing future post transitions to published. * * @since 2.3.0 * @access private * * @see wp_clear_scheduled_hook() * @global wpdb $wpdb WordPress database abstraction object. * * @param string $new_status New post status. * @param string $old_status Previous post status. * @param WP_Post $post Post object. */ function _transition_post_status( $new_status, $old_status, $post ) { global $wpdb; if ( 'publish' !== $old_status && 'publish' === $new_status ) { // Reset GUID if transitioning to publish and it is empty. if ( '' === get_the_guid( $post->ID ) ) { $wpdb->update( $wpdb->posts, array( 'guid' => get_permalink( $post->ID ) ), array( 'ID' => $post->ID ) ); } /** * Fires when a post's status is transitioned from private to published. * * @since 1.5.0 * @deprecated 2.3.0 Use {@see 'private_to_publish'} instead. * * @param int $post_id Post ID. */ do_action_deprecated( 'private_to_published', array( $post->ID ), '2.3.0', 'private_to_publish' ); } // If published posts changed clear the lastpostmodified cache. if ( 'publish' === $new_status || 'publish' === $old_status ) { foreach ( array( 'server', 'gmt', 'blog' ) as $timezone ) { wp_cache_delete( "lastpostmodified:$timezone", 'timeinfo' ); wp_cache_delete( "lastpostdate:$timezone", 'timeinfo' ); wp_cache_delete( "lastpostdate:$timezone:{$post->post_type}", 'timeinfo' ); } } if ( $new_status !== $old_status ) { wp_cache_delete( _count_posts_cache_key( $post->post_type ), 'counts' ); wp_cache_delete( _count_posts_cache_key( $post->post_type, 'readable' ), 'counts' ); } // Always clears the hook in case the post status bounced from future to draft. wp_clear_scheduled_hook( 'publish_future_post', array( $post->ID ) ); } /** * Hook used to schedule publication for a post marked for the future. * * The $post properties used and must exist are 'ID' and 'post_date_gmt'. * * @since 2.3.0 * @access private * * @param int $deprecated Not used. Can be set to null. Never implemented. Not marked * as deprecated with _deprecated_argument() as it conflicts with * wp_transition_post_status() and the default filter for _future_post_hook(). * @param WP_Post $post Post object. */ function _future_post_hook( $deprecated, $post ) { wp_clear_scheduled_hook( 'publish_future_post', array( $post->ID ) ); wp_schedule_single_event( strtotime( get_gmt_from_date( $post->post_date ) . ' GMT' ), 'publish_future_post', array( $post->ID ) ); } /** * Hook to schedule pings and enclosures when a post is published. * * Uses XMLRPC_REQUEST and WP_IMPORTING constants. * * @since 2.3.0 * @access private * * @param int $post_id The ID of the post being published. */ function _publish_post_hook( $post_id ) { if ( defined( 'XMLRPC_REQUEST' ) ) { /** * Fires when _publish_post_hook() is called during an XML-RPC request. * * @since 2.1.0 * * @param int $post_id Post ID. */ do_action( 'xmlrpc_publish_post', $post_id ); } if ( defined( 'WP_IMPORTING' ) ) { return; } if ( get_option( 'default_pingback_flag' ) ) { add_post_meta( $post_id, '_pingme', '1', true ); } add_post_meta( $post_id, '_encloseme', '1', true ); $to_ping = get_to_ping( $post_id ); if ( ! empty( $to_ping ) ) { add_post_meta( $post_id, '_trackbackme', '1' ); } if ( ! wp_next_scheduled( 'do_pings' ) ) { wp_schedule_single_event( time(), 'do_pings' ); } } /** * Returns the ID of the post's parent. * * @since 3.1.0 * @since 5.9.0 The `$post` parameter was made optional. * * @param int|WP_Post|null $post Optional. Post ID or post object. Defaults to global $post. * @return int|false Post parent ID (which can be 0 if there is no parent), * or false if the post does not exist. */ function wp_get_post_parent_id( $post = null ) { $post = get_post( $post ); if ( ! $post || is_wp_error( $post ) ) { return false; } return (int) $post->post_parent; } /** * Check the given subset of the post hierarchy for hierarchy loops. * * Prevents loops from forming and breaks those that it finds. Attached * to the {@see 'wp_insert_post_parent'} filter. * * @since 3.1.0 * * @see wp_find_hierarchy_loop() * * @param int $post_parent ID of the parent for the post we're checking. * @param int $post_ID ID of the post we're checking. * @return int The new post_parent for the post, 0 otherwise. */ function wp_check_post_hierarchy_for_loops( $post_parent, $post_ID ) { // Nothing fancy here - bail. if ( ! $post_parent ) { return 0; } // New post can't cause a loop. if ( ! $post_ID ) { return $post_parent; } // Can't be its own parent. if ( $post_parent == $post_ID ) { return 0; } // Now look for larger loops. $loop = wp_find_hierarchy_loop( 'wp_get_post_parent_id', $post_ID, $post_parent ); if ( ! $loop ) { return $post_parent; // No loop. } // Setting $post_parent to the given value causes a loop. if ( isset( $loop[ $post_ID ] ) ) { return 0; } // There's a loop, but it doesn't contain $post_ID. Break the loop. foreach ( array_keys( $loop ) as $loop_member ) { wp_update_post( array( 'ID' => $loop_member, 'post_parent' => 0, ) ); } return $post_parent; } /** * Sets the post thumbnail (featured image) for the given post. * * @since 3.1.0 * * @param int|WP_Post $post Post ID or post object where thumbnail should be attached. * @param int $thumbnail_id Thumbnail to attach. * @return int|bool True on success, false on failure. */ function set_post_thumbnail( $post, $thumbnail_id ) { $post = get_post( $post ); $thumbnail_id = absint( $thumbnail_id ); if ( $post && $thumbnail_id && get_post( $thumbnail_id ) ) { if ( wp_get_attachment_image( $thumbnail_id, 'thumbnail' ) ) { return update_post_meta( $post->ID, '_thumbnail_id', $thumbnail_id ); } else { return delete_post_meta( $post->ID, '_thumbnail_id' ); } } return false; } /** * Removes the thumbnail (featured image) from the given post. * * @since 3.3.0 * * @param int|WP_Post $post Post ID or post object from which the thumbnail should be removed. * @return bool True on success, false on failure. */ function delete_post_thumbnail( $post ) { $post = get_post( $post ); if ( $post ) { return delete_post_meta( $post->ID, '_thumbnail_id' ); } return false; } /** * Delete auto-drafts for new posts that are > 7 days old. * * @since 3.4.0 * * @global wpdb $wpdb WordPress database abstraction object. */ function wp_delete_auto_drafts() { global $wpdb; // Cleanup old auto-drafts more than 7 days old. $old_posts = $wpdb->get_col( "SELECT ID FROM $wpdb->posts WHERE post_status = 'auto-draft' AND DATE_SUB( NOW(), INTERVAL 7 DAY ) > post_date" ); foreach ( (array) $old_posts as $delete ) { // Force delete. wp_delete_post( $delete, true ); } } /** * Queues posts for lazy-loading of term meta. * * @since 4.5.0 * * @param array $posts Array of WP_Post objects. */ function wp_queue_posts_for_term_meta_lazyload( $posts ) { $post_type_taxonomies = array(); $term_ids = array(); foreach ( $posts as $post ) { if ( ! ( $post instanceof WP_Post ) ) { continue; } if ( ! isset( $post_type_taxonomies[ $post->post_type ] ) ) { $post_type_taxonomies[ $post->post_type ] = get_object_taxonomies( $post->post_type ); } foreach ( $post_type_taxonomies[ $post->post_type ] as $taxonomy ) { // Term cache should already be primed by `update_post_term_cache()`. $terms = get_object_term_cache( $post->ID, $taxonomy ); if ( false !== $terms ) { foreach ( $terms as $term ) { if ( ! in_array( $term->term_id, $term_ids, true ) ) { $term_ids[] = $term->term_id; } } } } } if ( $term_ids ) { $lazyloader = wp_metadata_lazyloader(); $lazyloader->queue_objects( 'term', $term_ids ); } } /** * Update the custom taxonomies' term counts when a post's status is changed. * * For example, default posts term counts (for custom taxonomies) don't include * private / draft posts. * * @since 3.3.0 * @access private * * @param string $new_status New post status. * @param string $old_status Old post status. * @param WP_Post $post Post object. */ function _update_term_count_on_transition_post_status( $new_status, $old_status, $post ) { // Update counts for the post's terms. foreach ( (array) get_object_taxonomies( $post->post_type ) as $taxonomy ) { $tt_ids = wp_get_object_terms( $post->ID, $taxonomy, array( 'fields' => 'tt_ids' ) ); wp_update_term_count( $tt_ids, $taxonomy ); } } /** * Adds any posts from the given IDs to the cache that do not already exist in cache. * * @since 3.4.0 * @access private * * @see update_post_caches() * * @global wpdb $wpdb WordPress database abstraction object. * * @param array $ids ID list. * @param bool $update_term_cache Optional. Whether to update the term cache. Default true. * @param bool $update_meta_cache Optional. Whether to update the meta cache. Default true. */ function _prime_post_caches( $ids, $update_term_cache = true, $update_meta_cache = true ) { global $wpdb; $non_cached_ids = _get_non_cached_ids( $ids, 'posts' ); if ( ! empty( $non_cached_ids ) ) { $fresh_posts = $wpdb->get_results( sprintf( "SELECT $wpdb->posts.* FROM $wpdb->posts WHERE ID IN (%s)", implode( ',', $non_cached_ids ) ) ); update_post_caches( $fresh_posts, 'any', $update_term_cache, $update_meta_cache ); } } /** * Adds a suffix if any trashed posts have a given slug. * * Store its desired (i.e. current) slug so it can try to reclaim it * if the post is untrashed. * * For internal use. * * @since 4.5.0 * @access private * * @param string $post_name Post slug. * @param int $post_ID Optional. Post ID that should be ignored. Default 0. */ function wp_add_trashed_suffix_to_post_name_for_trashed_posts( $post_name, $post_ID = 0 ) { $trashed_posts_with_desired_slug = get_posts( array( 'name' => $post_name, 'post_status' => 'trash', 'post_type' => 'any', 'nopaging' => true, 'post__not_in' => array( $post_ID ), ) ); if ( ! empty( $trashed_posts_with_desired_slug ) ) { foreach ( $trashed_posts_with_desired_slug as $_post ) { wp_add_trashed_suffix_to_post_name_for_post( $_post ); } } } /** * Adds a trashed suffix for a given post. * * Store its desired (i.e. current) slug so it can try to reclaim it * if the post is untrashed. * * For internal use. * * @since 4.5.0 * @access private * * @param WP_Post $post The post. * @return string New slug for the post. */ function wp_add_trashed_suffix_to_post_name_for_post( $post ) { global $wpdb; $post = get_post( $post ); if ( '__trashed' === substr( $post->post_name, -9 ) ) { return $post->post_name; } add_post_meta( $post->ID, '_wp_desired_post_slug', $post->post_name ); $post_name = _truncate_post_slug( $post->post_name, 191 ) . '__trashed'; $wpdb->update( $wpdb->posts, array( 'post_name' => $post_name ), array( 'ID' => $post->ID ) ); clean_post_cache( $post->ID ); return $post_name; } /** * Filters the SQL clauses of an attachment query to include filenames. * * @since 4.7.0 * @access private * * @global wpdb $wpdb WordPress database abstraction object. * * @param string[] $clauses An array including WHERE, GROUP BY, JOIN, ORDER BY, * DISTINCT, fields (SELECT), and LIMITS clauses. * @return string[] The modified array of clauses. */ function _filter_query_attachment_filenames( $clauses ) { global $wpdb; remove_filter( 'posts_clauses', __FUNCTION__ ); // Add a LEFT JOIN of the postmeta table so we don't trample existing JOINs. $clauses['join'] .= " LEFT JOIN {$wpdb->postmeta} AS sq1 ON ( {$wpdb->posts}.ID = sq1.post_id AND sq1.meta_key = '_wp_attached_file' )"; $clauses['groupby'] = "{$wpdb->posts}.ID"; $clauses['where'] = preg_replace( "/\({$wpdb->posts}.post_content (NOT LIKE|LIKE) (\'[^']+\')\)/", '$0 OR ( sq1.meta_value $1 $2 )', $clauses['where'] ); return $clauses; } /** * Sets the last changed time for the 'posts' cache group. * * @since 5.0.0 */ function wp_cache_set_posts_last_changed() { wp_cache_set( 'last_changed', microtime(), 'posts' ); } /** * Get all available post MIME types for a given post type. * * @since 2.5.0 * * @global wpdb $wpdb WordPress database abstraction object. * * @param string $type * @return mixed */ function get_available_post_mime_types( $type = 'attachment' ) { global $wpdb; $types = $wpdb->get_col( $wpdb->prepare( "SELECT DISTINCT post_mime_type FROM $wpdb->posts WHERE post_type = %s", $type ) ); return $types; } /** * Retrieves the path to an uploaded image file. * * Similar to `get_attached_file()` however some images may have been processed after uploading * to make them suitable for web use. In this case the attached "full" size file is usually replaced * with a scaled down version of the original image. This function always returns the path * to the originally uploaded image file. * * @since 5.3.0 * @since 5.4.0 Added the `$unfiltered` parameter. * * @param int $attachment_id Attachment ID. * @param bool $unfiltered Optional. Passed through to `get_attached_file()`. Default false. * @return string|false Path to the original image file or false if the attachment is not an image. */ function wp_get_original_image_path( $attachment_id, $unfiltered = false ) { if ( ! wp_attachment_is_image( $attachment_id ) ) { return false; } $image_meta = wp_get_attachment_metadata( $attachment_id ); $image_file = get_attached_file( $attachment_id, $unfiltered ); if ( empty( $image_meta['original_image'] ) ) { $original_image = $image_file; } else { $original_image = path_join( dirname( $image_file ), $image_meta['original_image'] ); } /** * Filters the path to the original image. * * @since 5.3.0 * * @param string $original_image Path to original image file. * @param int $attachment_id Attachment ID. */ return apply_filters( 'wp_get_original_image_path', $original_image, $attachment_id ); } /** * Retrieve the URL to an original attachment image. * * Similar to `wp_get_attachment_url()` however some images may have been * processed after uploading. In this case this function returns the URL * to the originally uploaded image file. * * @since 5.3.0 * * @param int $attachment_id Attachment post ID. * @return string|false Attachment image URL, false on error or if the attachment is not an image. */ function wp_get_original_image_url( $attachment_id ) { if ( ! wp_attachment_is_image( $attachment_id ) ) { return false; } $image_url = wp_get_attachment_url( $attachment_id ); if ( ! $image_url ) { return false; } $image_meta = wp_get_attachment_metadata( $attachment_id ); if ( empty( $image_meta['original_image'] ) ) { $original_image_url = $image_url; } else { $original_image_url = path_join( dirname( $image_url ), $image_meta['original_image'] ); } /** * Filters the URL to the original attachment image. * * @since 5.3.0 * * @param string $original_image_url URL to original image. * @param int $attachment_id Attachment ID. */ return apply_filters( 'wp_get_original_image_url', $original_image_url, $attachment_id ); } /** * Filter callback which sets the status of an untrashed post to its previous status. * * This can be used as a callback on the `wp_untrash_post_status` filter. * * @since 5.6.0 * * @param string $new_status The new status of the post being restored. * @param int $post_id The ID of the post being restored. * @param string $previous_status The status of the post at the point where it was trashed. * @return string The new status of the post. */ function wp_untrash_post_set_previous_status( $new_status, $post_id, $previous_status ) { return $previous_status; }