addingAwareAlignments']; /* * Reset default browser margin on the root body element. * This is set on the root selector **before** generating the ruleset * from the `theme.json`. This is to ensure that if the `theme.json` declares * `margin` in its `spacing` declaration for the `body` element then these * user-generated values take precedence in the CSS cascade. * @link https://github.com/WordPress/gutenberg/issues/36147. */ $css .= 'body { margin: 0;'; /* * If there are content and wide widths in theme.json, output them * as custom properties on the body element so all blocks can use them. */ if ( isset( $settings['layout']['contentSize'] ) || isset( $settings['layout']['wideSize'] ) ) { $content_size = isset( $settings['layout']['contentSize'] ) ? $settings['layout']['contentSize'] : $settings['layout']['wideSize']; $content_size = static::is_safe_css_declaration( 'max-width', $content_size ) ? $content_size : 'initial'; $wide_size = isset( $settings['layout']['wideSize'] ) ? $settings['layout']['wideSize'] : $settings['layout']['contentSize']; $wide_size = static::is_safe_css_declaration( 'max-width', $wide_size ) ? $wide_size : 'initial'; $css .= '--wp--style--global--content-size: ' . $content_size . ';'; $css .= '--wp--style--global--wide-size: ' . $wide_size . ';'; } $css .= ' }'; if ( $use_root_padding ) { // Top and bottom padding are applied to the outer block container. $css .= '.wp-site-blocks { padding-top: var(--wp--style--root--padding-top); padding-bottom: var(--wp--style--root--padding-bottom); }'; // Right and left padding are applied to the first container with `.has-global-padding` class. $css .= '.has-global-padding { padding-right: var(--wp--style--root--padding-right); padding-left: var(--wp--style--root--padding-left); }'; // Nested containers with `.has-global-padding` class do not get padding. $css .= '.has-global-padding :where(.has-global-padding:not(.wp-block-block)) { padding-right: 0; padding-left: 0; }'; // Alignfull children of the container with left and right padding have negative margins so they can still be full width. $css .= '.has-global-padding > .alignfull { margin-right: calc(var(--wp--style--root--padding-right) * -1); margin-left: calc(var(--wp--style--root--padding-left) * -1); }'; // The above rule is negated for alignfull children of nested containers. $css .= '.has-global-padding :where(.has-global-padding:not(.wp-block-block)) > .alignfull { margin-right: 0; margin-left: 0; }'; // Some of the children of alignfull blocks without content width should also get padding: text blocks and non-alignfull container blocks. $css .= '.has-global-padding > .alignfull:where(:not(.has-global-padding):not(.is-layout-flex):not(.is-layout-grid)) > :where([class*="wp-block-"]:not(.alignfull):not([class*="__"]),p,h1,h2,h3,h4,h5,h6,ul,ol) { padding-right: var(--wp--style--root--padding-right); padding-left: var(--wp--style--root--padding-left); }'; // The above rule also has to be negated for blocks inside nested `.has-global-padding` blocks. $css .= '.has-global-padding :where(.has-global-padding) > .alignfull:where(:not(.has-global-padding)) > :where([class*="wp-block-"]:not(.alignfull):not([class*="__"]),p,h1,h2,h3,h4,h5,h6,ul,ol) { padding-right: 0; padding-left: 0; }'; } $css .= '.wp-site-blocks > .alignleft { float: left; margin-right: 2em; }'; $css .= '.wp-site-blocks > .alignright { float: right; margin-left: 2em; }'; $css .= '.wp-site-blocks > .aligncenter { justify-content: center; margin-left: auto; margin-right: auto; }'; $block_gap_value = isset( $this->theme_json['styles']['spacing']['blockGap'] ) ? $this->theme_json['styles']['spacing']['blockGap'] : '0.5em'; $has_block_gap_support = isset( $this->theme_json['settings']['spacing']['blockGap'] ); if ( $has_block_gap_support ) { $block_gap_value = static::get_property_value( $this->theme_json, array( 'styles', 'spacing', 'blockGap' ) ); $css .= ":where(.wp-site-blocks) > * { margin-block-start: $block_gap_value; margin-block-end: 0; }"; $css .= ':where(.wp-site-blocks) > :first-child:first-child { margin-block-start: 0; }'; $css .= ':where(.wp-site-blocks) > :last-child:last-child { margin-block-end: 0; }'; // For backwards compatibility, ensure the legacy block gap CSS variable is still available. $css .= "$selector { --wp--style--block-gap: $block_gap_value; }"; } $css .= $this->get_layout_styles( $block_metadata ); return $css; } /** * For metadata values that can either be booleans or paths to booleans, gets the value. * * $data = array( * 'color' => array( * 'defaultPalette' => true * ) * ); * * static::get_metadata_boolean( $data, false ); * // => false * * static::get_metadata_boolean( $data, array( 'color', 'defaultPalette' ) ); * // => true * * @since 6.0.0 * * @param array $data The data to inspect. * @param bool|array $path Boolean or path to a boolean. * @param bool $default_value Default value if the referenced path is missing. * Default false. * @return bool Value of boolean metadata. */ protected static function get_metadata_boolean( $data, $path, $default_value = false ) { if ( is_bool( $path ) ) { return $path; } if ( is_array( $path ) ) { $value = _wp_array_get( $data, $path ); if ( null !== $value ) { return $value; } } return $default_value; } /** * Merges new incoming data. * * @since 5.8.0 * @since 5.9.0 Duotone preset also has origins. * * @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 ) { // Replace the spacing.units. $path = $node['path']; $path[] = 'spacing'; $path[] = 'units'; $content = _wp_array_get( $incoming_data, $path, null ); if ( isset( $content ) ) { _wp_array_set( $this->theme_json, $path, $content ); } // Replace the presets. foreach ( static::PRESETS_METADATA as $preset ) { $override_preset = ! static::get_metadata_boolean( $this->theme_json['settings'], $preset['prevent_override'], true ); foreach ( static::VALID_ORIGINS as $origin ) { $base_path = $node['path']; foreach ( $preset['path'] as $leaf ) { $base_path[] = $leaf; } $path = $base_path; $path[] = $origin; $content = _wp_array_get( $incoming_data, $path, null ); if ( ! isset( $content ) ) { continue; } if ( 'theme' === $origin && $preset['use_default_names'] ) { foreach ( $content as $key => $item ) { if ( ! isset( $item['name'] ) ) { $name = static::get_name_from_defaults( $item['slug'], $base_path ); if ( null !== $name ) { $content[ $key ]['name'] = $name; } } } } if ( ( 'theme' !== $origin ) || ( 'theme' === $origin && $override_preset ) ) { _wp_array_set( $this->theme_json, $path, $content ); } else { $slugs_node = static::get_default_slugs( $this->theme_json, $node['path'] ); $slugs = array_merge_recursive( $slugs_global, $slugs_node ); $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; } /** * Determines whether a presets should be overridden or not. * * @since 5.9.0 * @deprecated 6.0.0 Use {@see 'get_metadata_boolean'} instead. * * @param array $theme_json The theme.json like structure to inspect. * @param array $path Path to inspect. * @param bool|array $override Data to compute whether to override the preset. * @return bool */ protected static function should_override_preset( $theme_json, $path, $override ) { _deprecated_function( __METHOD__, '6.0.0', 'get_metadata_boolean' ); if ( is_bool( $override ) ) { return $override; } /* * The relationship between whether to override the defaults * and whether the defaults are enabled is inverse: * * - If defaults are enabled => theme presets should not be overridden * - If defaults are disabled => theme presets should be overridden * * For example, a theme sets defaultPalette to false, * making the default palette hidden from the user. * In that case, we want all the theme presets to be present, * so they should override the defaults. */ if ( is_array( $override ) ) { $value = _wp_array_get( $theme_json, array_merge( $path, $override ) ); if ( isset( $value ) ) { return ! $value; } // Search the top-level key if none was found for this node. $value = _wp_array_get( $theme_json, array_merge( array( 'settings' ), $override ) ); if ( isset( $value ) ) { return ! $value; } return true; } } /** * Returns the default slugs for all the presets in an associative array * whose keys are the preset paths and the 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 = $node_path; foreach ( $metadata['path'] as $leaf ) { $path[] = $leaf; } $path[] = 'default'; $preset = _wp_array_get( $data, $path, null ); if ( ! isset( $preset ) ) { continue; } $slugs_for_preset = array(); foreach ( $preset as $item ) { if ( isset( $item['slug'] ) ) { $slugs_for_preset[] = $item['slug']; } } _wp_array_set( $slugs, $metadata['path'], $slugs_for_preset ); } return $slugs; } /** * Gets a `default`'s preset name by a provided slug. * * @since 5.9.0 * * @param string $slug The slug we want to find a match from default presets. * @param array $base_path The path to inspect. It's 'settings' by default. * @return string|null */ protected function get_name_from_defaults( $slug, $base_path ) { $path = $base_path; $path[] = 'default'; $default_content = _wp_array_get( $this->theme_json, $path, null ); if ( ! $default_content ) { return null; } foreach ( $default_content as $item ) { if ( $slug === $item['slug'] ) { return $item['name']; } } return null; } /** * Removes the preset values whose slug is equal to any of given slugs. * * @since 5.9.0 * * @param array $node The node with the presets to validate. * @param array $slugs The slugs that should not be overridden. * @return array The new node. */ protected static function filter_slugs( $node, $slugs ) { if ( empty( $slugs ) ) { return $node; } $new_node = array(); foreach ( $node as $value ) { if ( isset( $value['slug'] ) && ! in_array( $value['slug'], $slugs, true ) ) { $new_node[] = $value; } } return $new_node; } /** * Removes insecure data from theme.json. * * @since 5.9.0 * @since 6.3.2 Preserves global styles block variations when securing styles. * * @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 ); $valid_variations = array(); foreach ( self::get_blocks_metadata() as $block_name => $block_meta ) { if ( ! isset( $block_meta['styleVariations'] ) ) { continue; } $valid_variations[ $block_name ] = array_keys( $block_meta['styleVariations'] ); } $theme_json = static::sanitize( $theme_json, $valid_block_names, $valid_element_names, $valid_variations ); $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; } // The global styles custom CSS is not sanitized, but can only be edited by users with 'edit_css' capability. if ( isset( $input['css'] ) && current_user_can( 'edit_css' ) ) { $output = $input; } else { $output = static::remove_insecure_styles( $input ); } /* * Get a reference to element name from path. * $metadata['path'] = array( 'styles', 'elements', 'link' ); */ $current_element = $metadata['path'][ count( $metadata['path'] ) - 1 ]; /* * $output is stripped of pseudo selectors. Re-add and process them * or insecure styles here. */ if ( isset( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $current_element ] ) ) { foreach ( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $current_element ] as $pseudo_selector ) { if ( isset( $input[ $pseudo_selector ] ) ) { $output[ $pseudo_selector ] = static::remove_insecure_styles( $input[ $pseudo_selector ] ); } } } if ( ! empty( $output ) ) { _wp_array_set( $sanitized, $metadata['path'], $output ); } if ( isset( $metadata['variations'] ) ) { foreach ( $metadata['variations'] as $variation ) { $variation_input = _wp_array_get( $theme_json, $variation['path'], array() ); if ( empty( $variation_input ) ) { continue; } $variation_output = static::remove_insecure_styles( $variation_input ); if ( ! empty( $variation_output ) ) { _wp_array_set( $sanitized, $variation['path'], $variation_output ); } } } } $setting_nodes = static::get_setting_nodes( $theme_json ); foreach ( $setting_nodes as $metadata ) { $input = _wp_array_get( $theme_json, $metadata['path'], array() ); if ( empty( $input ) ) { continue; } $output = static::remove_insecure_settings( $input ); if ( ! empty( $output ) ) { _wp_array_set( $sanitized, $metadata['path'], $output ); } } if ( empty( $sanitized['styles'] ) ) { unset( $theme_json['styles'] ); } else { $theme_json['styles'] = $sanitized['styles']; } if ( empty( $sanitized['settings'] ) ) { unset( $theme_json['settings'] ); } else { $theme_json['settings'] = $sanitized['settings']; } return $theme_json; } /** * Processes a setting node and returns the same node * without the insecure settings. * * @since 5.9.0 * * @param array $input Node to process. * @return array */ protected static function remove_insecure_settings( $input ) { $output = array(); foreach ( static::PRESETS_METADATA as $preset_metadata ) { foreach ( static::VALID_ORIGINS as $origin ) { $path_with_origin = $preset_metadata['path']; $path_with_origin[] = $origin; $presets = _wp_array_get( $input, $path_with_origin, null ); if ( null === $presets ) { continue; } $escaped_preset = array(); foreach ( $presets as $preset ) { if ( esc_attr( esc_html( $preset['name'] ) ) === $preset['name'] && sanitize_html_class( $preset['slug'] ) === $preset['slug'] ) { $value = null; if ( isset( $preset_metadata['value_key'], $preset[ $preset_metadata['value_key'] ] ) ) { $value = $preset[ $preset_metadata['value_key'] ]; } elseif ( isset( $preset_metadata['value_func'] ) && is_callable( $preset_metadata['value_func'] ) ) { $value = call_user_func( $preset_metadata['value_func'], $preset ); } $preset_is_valid = true; foreach ( $preset_metadata['properties'] as $property ) { if ( ! static::is_safe_css_declaration( $property, $value ) ) { $preset_is_valid = false; break; } } if ( $preset_is_valid ) { $escaped_preset[] = $preset; } } } if ( ! empty( $escaped_preset ) ) { _wp_array_set( $output, $path_with_origin, $escaped_preset ); } } } // Ensure indirect properties not included in any `PRESETS_METADATA` value are allowed. static::remove_indirect_properties( $input, $output ); return $output; } /** * Processes a style node and returns the same node * without the insecure styles. * * @since 5.9.0 * * @param array $input Node to process. * @return array */ protected static function remove_insecure_styles( $input ) { $output = array(); $declarations = static::compute_style_properties( $input ); foreach ( $declarations as $declaration ) { if ( static::is_safe_css_declaration( $declaration['name'], $declaration['value'] ) ) { $path = static::PROPERTIES_METADATA[ $declaration['name'] ]; /* * Check the value isn't an array before adding so as to not * double up shorthand and longhand styles. */ $value = _wp_array_get( $input, $path, array() ); if ( ! is_array( $value ) ) { _wp_array_set( $output, $path, $value ); } } } // Ensure indirect properties not handled by `compute_style_properties` are allowed. static::remove_indirect_properties( $input, $output ); return $output; } /** * Checks that a declaration provided by the user is safe. * * @since 5.9.0 * * @param string $property_name Property name in a CSS declaration, i.e. the `color` in `color: red`. * @param string $property_value Value in a CSS declaration, i.e. the `red` in `color: red`. * @return bool */ protected static function is_safe_css_declaration( $property_name, $property_value ) { $style_to_validate = $property_name . ': ' . $property_value; $filtered = esc_html( safecss_filter_attr( $style_to_validate ) ); return ! empty( trim( $filtered ) ); } /** * Removes indirect properties from the given input node and * sets in the given output node. * * @since 6.2.0 * * @param array $input Node to process. * @param array $output The processed node. Passed by reference. */ private static function remove_indirect_properties( $input, &$output ) { foreach ( static::INDIRECT_PROPERTIES_METADATA as $property => $paths ) { foreach ( $paths as $path ) { $value = _wp_array_get( $input, $path ); if ( is_string( $value ) && static::is_safe_css_declaration( $property, $value ) ) { _wp_array_set( $output, $path, $value ); } } } } /** * Returns the raw data. * * @since 5.8.0 * * @return array Raw data. */ public function get_raw_data() { return $this->theme_json; } /** * Transforms the given editor settings according the * add_theme_support format to the theme.json format. * * @since 5.8.0 * * @param array $settings Existing editor settings. * @return array Config that adheres to the theme.json schema. */ public static function get_from_editor_settings( $settings ) { $theme_settings = array( 'version' => static::LATEST_SCHEMA, 'settings' => array(), ); // Deprecated theme supports. if ( isset( $settings['disableCustomColors'] ) ) { 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 = $node['path']; foreach ( $preset_metadata['path'] as $preset_metadata_path ) { $path[] = $preset_metadata_path; } $preset = _wp_array_get( $output, $path, null ); if ( null === $preset ) { continue; } $items = array(); if ( isset( $preset['theme'] ) ) { foreach ( $preset['theme'] as $item ) { $slug = $item['slug']; unset( $item['slug'] ); $items[ $slug ] = $item; } } if ( isset( $preset['custom'] ) ) { foreach ( $preset['custom'] as $item ) { $slug = $item['slug']; unset( $item['slug'] ); $items[ $slug ] = $item; } } $flattened_preset = array(); foreach ( $items as $slug => $value ) { $flattened_preset[] = array_merge( array( 'slug' => (string) $slug ), $value ); } _wp_array_set( $output, $path, $flattened_preset ); } } /* * If all of the static::APPEARANCE_TOOLS_OPT_INS are true, * this code unsets them and sets 'appearanceTools' instead. */ foreach ( $nodes as $node ) { $all_opt_ins_are_set = true; foreach ( static::APPEARANCE_TOOLS_OPT_INS as $opt_in_path ) { $full_path = $node['path']; foreach ( $opt_in_path as $opt_in_path_item ) { $full_path[] = $opt_in_path_item; } /* * Use "unset prop" as a marker instead of "null" because * "null" can be a valid value for some props (e.g. blockGap). */ $opt_in_value = _wp_array_get( $output, $full_path, 'unset prop' ); if ( 'unset prop' === $opt_in_value ) { $all_opt_ins_are_set = false; break; } } if ( $all_opt_ins_are_set ) { $node_path_with_appearance_tools = $node['path']; $node_path_with_appearance_tools[] = 'appearanceTools'; _wp_array_set( $output, $node_path_with_appearance_tools, true ); foreach ( static::APPEARANCE_TOOLS_OPT_INS as $opt_in_path ) { $full_path = $node['path']; foreach ( $opt_in_path as $opt_in_path_item ) { $full_path[] = $opt_in_path_item; } /* * Use "unset prop" as a marker instead of "null" because * "null" can be a valid value for some props (e.g. blockGap). */ $opt_in_value = _wp_array_get( $output, $full_path, 'unset prop' ); if ( true !== $opt_in_value ) { continue; } /* * The following could be improved to be path independent. * At the moment it relies on a couple of assumptions: * * - all opt-ins having a path of size 2. * - there's two sources of settings: the top-level and the block-level. */ if ( ( 1 === count( $node['path'] ) ) && ( 'settings' === $node['path'][0] ) ) { // Top-level settings. unset( $output['settings'][ $opt_in_path[0] ][ $opt_in_path[1] ] ); if ( empty( $output['settings'][ $opt_in_path[0] ] ) ) { unset( $output['settings'][ $opt_in_path[0] ] ); } } elseif ( ( 3 === count( $node['path'] ) ) && ( 'settings' === $node['path'][0] ) && ( 'blocks' === $node['path'][1] ) ) { // Block-level settings. $block_name = $node['path'][2]; unset( $output['settings']['blocks'][ $block_name ][ $opt_in_path[0] ][ $opt_in_path[1] ] ); if ( empty( $output['settings']['blocks'][ $block_name ][ $opt_in_path[0] ] ) ) { unset( $output['settings']['blocks'][ $block_name ][ $opt_in_path[0] ] ); } } } } } wp_recursive_ksort( $output ); return $output; } /** * Sets the spacingSizes array based on the spacingScale values from theme.json. * * @since 6.1.0 * * @return null|void */ public function set_spacing_sizes() { $spacing_scale = isset( $this->theme_json['settings']['spacing']['spacingScale'] ) ? $this->theme_json['settings']['spacing']['spacingScale'] : array(); if ( ! isset( $spacing_scale['steps'] ) || ! is_numeric( $spacing_scale['steps'] ) || ! isset( $spacing_scale['mediumStep'] ) || ! isset( $spacing_scale['unit'] ) || ! isset( $spacing_scale['operator'] ) || ! isset( $spacing_scale['increment'] ) || ! isset( $spacing_scale['steps'] ) || ! is_numeric( $spacing_scale['increment'] ) || ! is_numeric( $spacing_scale['mediumStep'] ) || ( '+' !== $spacing_scale['operator'] && '*' !== $spacing_scale['operator'] ) ) { if ( ! empty( $spacing_scale ) ) { trigger_error( sprintf( /* translators: 1: theme.json, 2: settings.spacing.spacingScale */ __( 'Some of the %1$s %2$s values are invalid' ), 'theme.json', 'settings.spacing.spacingScale' ), E_USER_NOTICE ); } return null; } // If theme authors want to prevent the generation of the core spacing scale they can set their theme.json spacingScale.steps to 0. if ( 0 === $spacing_scale['steps'] ) { return null; } $unit = '%' === $spacing_scale['unit'] ? '%' : sanitize_title( $spacing_scale['unit'] ); $current_step = $spacing_scale['mediumStep']; $steps_mid_point = round( $spacing_scale['steps'] / 2, 0 ); $x_small_count = null; $below_sizes = array(); $slug = 40; $remainder = 0; for ( $below_midpoint_count = $steps_mid_point - 1; $spacing_scale['steps'] > 1 && $slug > 0 && $below_midpoint_count > 0; $below_midpoint_count-- ) { if ( '+' === $spacing_scale['operator'] ) { $current_step -= $spacing_scale['increment']; } elseif ( $spacing_scale['increment'] > 1 ) { $current_step /= $spacing_scale['increment']; } else { $current_step *= $spacing_scale['increment']; } if ( $current_step <= 0 ) { $remainder = $below_midpoint_count; break; } $below_sizes[] = array( /* translators: %s: Digit to indicate multiple of sizing, eg. 2X-Small. */ 'name' => $below_midpoint_count === $steps_mid_point - 1 ? __( 'Small' ) : sprintf( __( '%sX-Small' ), (string) $x_small_count ), 'slug' => (string) $slug, 'size' => round( $current_step, 2 ) . $unit, ); if ( $below_midpoint_count === $steps_mid_point - 2 ) { $x_small_count = 2; } if ( $below_midpoint_count < $steps_mid_point - 2 ) { ++$x_small_count; } $slug -= 10; } $below_sizes = array_reverse( $below_sizes ); $below_sizes[] = array( 'name' => __( 'Medium' ), 'slug' => '50', 'size' => $spacing_scale['mediumStep'] . $unit, ); $current_step = $spacing_scale['mediumStep']; $x_large_count = null; $above_sizes = array(); $slug = 60; $steps_above = ( $spacing_scale['steps'] - $steps_mid_point ) + $remainder; for ( $above_midpoint_count = 0; $above_midpoint_count < $steps_above; $above_midpoint_count++ ) { $current_step = '+' === $spacing_scale['operator'] ? $current_step + $spacing_scale['increment'] : ( $spacing_scale['increment'] >= 1 ? $current_step * $spacing_scale['increment'] : $current_step / $spacing_scale['increment'] ); $above_sizes[] = array( /* translators: %s: Digit to indicate multiple of sizing, eg. 2X-Large. */ 'name' => 0 === $above_midpoint_count ? __( 'Large' ) : sprintf( __( '%sX-Large' ), (string) $x_large_count ), 'slug' => (string) $slug, 'size' => round( $current_step, 2 ) . $unit, ); if ( 1 === $above_midpoint_count ) { $x_large_count = 2; } if ( $above_midpoint_count > 1 ) { ++$x_large_count; } $slug += 10; } $spacing_sizes = $below_sizes; foreach ( $above_sizes as $above_sizes_item ) { $spacing_sizes[] = $above_sizes_item; } // If there are 7 or fewer steps in the scale revert to numbers for labels instead of t-shirt sizes. if ( $spacing_scale['steps'] <= 7 ) { for ( $spacing_sizes_count = 0; $spacing_sizes_count < count( $spacing_sizes ); $spacing_sizes_count++ ) { $spacing_sizes[ $spacing_sizes_count ]['name'] = (string) ( $spacing_sizes_count + 1 ); } } _wp_array_set( $this->theme_json, array( 'settings', 'spacing', 'spacingSizes', 'default' ), $spacing_sizes ); } /** * This is used to convert the internal representation of variables to the CSS representation. * For example, `var:preset|color|vivid-green-cyan` becomes `var(--wp--preset--color--vivid-green-cyan)`. * * @since 6.3.0 * @param string $value The variable such as var:preset|color|vivid-green-cyan to convert. * @return string The converted variable. */ private static function convert_custom_properties( $value ) { $prefix = 'var:'; $prefix_len = strlen( $prefix ); $token_in = '|'; $token_out = '--'; if ( str_starts_with( $value, $prefix ) ) { $unwrapped_name = str_replace( $token_in, $token_out, substr( $value, $prefix_len ) ); $value = "var(--wp--$unwrapped_name)"; } return $value; } /** * Given a tree, converts the internal representation of variables to the CSS representation. * It is recursive and modifies the input in-place. * * @since 6.3.0 * @param array $tree Input to process. * @return array The modified $tree. */ private static function resolve_custom_css_format( $tree ) { $prefix = 'var:'; foreach ( $tree as $key => $data ) { if ( is_string( $data ) && str_starts_with( $data, $prefix ) ) { $tree[ $key ] = self::convert_custom_properties( $data ); } elseif ( is_array( $data ) ) { $tree[ $key ] = self::resolve_custom_css_format( $data ); } } return $tree; } /** * Returns the selectors metadata for a block. * * @since 6.3.0 * * @param object $block_type The block type. * @param string $root_selector The block's root selector. * * @return array The custom selectors set by the block. */ protected static function get_block_selectors( $block_type, $root_selector ) { if ( ! empty( $block_type->selectors ) ) { return $block_type->selectors; } $selectors = array( 'root' => $root_selector ); foreach ( static::BLOCK_SUPPORT_FEATURE_LEVEL_SELECTORS as $key => $feature ) { $feature_selector = wp_get_block_css_selector( $block_type, $key ); if ( null !== $feature_selector ) { $selectors[ $feature ] = array( 'root' => $feature_selector ); } } return $selectors; } /** * Generates all the element selectors for a block. * * @since 6.3.0 * * @param string $root_selector The block's root CSS selector. * @return array The block's element selectors. */ protected static function get_block_element_selectors( $root_selector ) { /* * Assign defaults, then override those that the block sets by itself. * If the block selector is compounded, will append the element to each * individual block selector. */ $block_selectors = explode( ',', $root_selector ); $element_selectors = array(); foreach ( static::ELEMENTS as $el_name => $el_selector ) { $element_selector = array(); foreach ( $block_selectors as $selector ) { if ( $selector === $el_selector ) { $element_selector = array( $el_selector ); break; } $element_selector[] = static::prepend_to_selector( $el_selector, $selector . ' ' ); } $element_selectors[ $el_name ] = implode( ',', $element_selector ); } return $element_selectors; } /** * Generates style declarations for a node's features e.g., color, border, * typography etc. that have custom selectors in their related block's * metadata. * * @since 6.3.0 * * @param object $metadata The related block metadata containing selectors. * @param object $node A merged theme.json node for block or variation. * * @return array The style declarations for the node's features with custom * selectors. */ protected function get_feature_declarations_for_node( $metadata, &$node ) { $declarations = array(); if ( ! isset( $metadata['selectors'] ) ) { return $declarations; } $settings = isset( $this->theme_json['settings'] ) ? $this->theme_json['settings'] : array(); foreach ( $metadata['selectors'] as $feature => $feature_selectors ) { /* * Skip if this is the block's root selector or the block doesn't * have any styles for the feature. */ if ( 'root' === $feature || empty( $node[ $feature ] ) ) { continue; } if ( is_array( $feature_selectors ) ) { foreach ( $feature_selectors as $subfeature => $subfeature_selector ) { if ( 'root' === $subfeature || empty( $node[ $feature ][ $subfeature ] ) ) { continue; } /* * Create temporary node containing only the subfeature data * to leverage existing `compute_style_properties` function. */ $subfeature_node = array( $feature => array( $subfeature => $node[ $feature ][ $subfeature ], ), ); // Generate style declarations. $new_declarations = static::compute_style_properties( $subfeature_node, $settings, null, $this->theme_json ); // Merge subfeature declarations into feature declarations. if ( isset( $declarations[ $subfeature_selector ] ) ) { foreach ( $new_declarations as $new_declaration ) { $declarations[ $subfeature_selector ][] = $new_declaration; } } else { $declarations[ $subfeature_selector ] = $new_declarations; } /* * Remove the subfeature from the block's node now its * styles will be included under its own selector not the * block's. */ unset( $node[ $feature ][ $subfeature ] ); } } /* * Now subfeatures have been processed and removed we can process * feature root selector or simple string selector. */ if ( is_string( $feature_selectors ) || ( isset( $feature_selectors['root'] ) && $feature_selectors['root'] ) ) { $feature_selector = is_string( $feature_selectors ) ? $feature_selectors : $feature_selectors['root']; /* * Create temporary node containing only the feature data * to leverage existing `compute_style_properties` function. */ $feature_node = array( $feature => $node[ $feature ] ); // Generate the style declarations. $new_declarations = static::compute_style_properties( $feature_node, $settings, null, $this->theme_json ); /* * Merge new declarations with any that already exist for * the feature selector. This may occur when multiple block * support features use the same custom selector. */ if ( isset( $declarations[ $feature_selector ] ) ) { foreach ( $new_declarations as $new_declaration ) { $declarations[ $feature_selector ][] = $new_declaration; } } else { $declarations[ $feature_selector ] = $new_declarations; } /* * Remove the feature from the block's node now its styles * will be included under its own selector not the block's. */ unset( $node[ $feature ] ); } } return $declarations; } /** * Replaces CSS variables with their values in place. * * @since 6.3.0 * @since 6.5.0 Check for empty style before processing its value. * * @param array $styles CSS declarations to convert. * @param array $values key => value pairs to use for replacement. * @return array */ private static function convert_variables_to_value( $styles, $values ) { foreach ( $styles as $key => $style ) { if ( empty( $style ) ) { continue; } if ( is_array( $style ) ) { $styles[ $key ] = self::convert_variables_to_value( $style, $values ); continue; } if ( 0 <= strpos( $style, 'var(' ) ) { // find all the variables in the string in the form of var(--variable-name, fallback), with fallback in the second capture group. $has_matches = preg_match_all( '/var\(([^),]+)?,?\s?(\S+)?\)/', $style, $var_parts ); if ( $has_matches ) { $resolved_style = $styles[ $key ]; foreach ( $var_parts[1] as $index => $var_part ) { $key_in_values = 'var(' . $var_part . ')'; $rule_to_replace = $var_parts[0][ $index ]; // the css rule to replace e.g. var(--wp--preset--color--vivid-green-cyan). $fallback = $var_parts[2][ $index ]; // the fallback value. $resolved_style = str_replace( array( $rule_to_replace, $fallback, ), array( isset( $values[ $key_in_values ] ) ? $values[ $key_in_values ] : $rule_to_replace, isset( $values[ $fallback ] ) ? $values[ $fallback ] : $fallback, ), $resolved_style ); } $styles[ $key ] = $resolved_style; } } } return $styles; } /** * Resolves the values of CSS variables in the given styles. * * @since 6.3.0 * @param WP_Theme_JSON $theme_json The theme json resolver. * * @return WP_Theme_JSON The $theme_json with resolved variables. */ public static function resolve_variables( $theme_json ) { $settings = $theme_json->get_settings(); $styles = $theme_json->get_raw_data()['styles']; $preset_vars = static::compute_preset_vars( $settings, static::VALID_ORIGINS ); $theme_vars = static::compute_theme_vars( $settings ); $vars = array_reduce( array_merge( $preset_vars, $theme_vars ), function ( $carry, $item ) { $name = $item['name']; $carry[ "var({$name})" ] = $item['value']; return $carry; }, array() ); $theme_json->theme_json['styles'] = self::convert_variables_to_value( $styles, $vars ); return $theme_json; } /** * Generates a selector for a block style variation. * * @since 6.5.0 * * @param string $variation_name Name of the block style variation. * @param string $block_selector CSS selector for the block. * @return string Block selector with block style variation selector added to it. */ protected static function get_block_style_variation_selector( $variation_name, $block_selector ) { $variation_class = ".is-style-$variation_name"; if ( ! $block_selector ) { return $variation_class; } $limit = 1; $selector_parts = explode( ',', $block_selector ); $result = array(); foreach ( $selector_parts as $part ) { $result[] = preg_replace_callback( '/((?::\([^)]+\))?\s*)([^\s:]+)/', function ( $matches ) use ( $variation_class ) { return $matches[1] . $matches[2] . $variation_class; }, $part, $limit ); } return implode( ',', $result ); } } $request['post'] ) ) { $prepared_attachment->post_parent = (int) $request['post']; } return $prepared_attachment; } /** * Prepares a single attachment output for response. * * @since 4.7.0 * @since 5.9.0 Renamed `$post` to `$item` to match parent class for PHP 8 named parameter support. * * @param WP_Post $item Attachment object. * @param WP_REST_Request $request Request object. * @return WP_REST_Response Response object. */ public function prepare_item_for_response( $item, $request ) { // Restores the more descriptive, specific name for use within this method. $post = $item; $response = parent::prepare_item_for_response( $post, $request ); $fields = $this->get_fields_for_response( $request ); $data = $response->get_data(); if ( in_array( 'description', $fields, true ) ) { $data['description'] = array( 'raw' => $post->post_content, /** This filter is documented in wp-includes/post-template.php */ 'rendered' => apply_filters( 'the_content', $post->post_content ), ); } if ( in_array( 'caption', $fields, true ) ) { /** This filter is documented in wp-includes/post-template.php */ $caption = apply_filters( 'get_the_excerpt', $post->post_excerpt, $post ); /** This filter is documented in wp-includes/post-template.php */ $caption = apply_filters( 'the_excerpt', $caption ); $data['caption'] = array( 'raw' => $post->post_excerpt, 'rendered' => $caption, ); } if ( in_array( 'alt_text', $fields, true ) ) { $data['alt_text'] = get_post_meta( $post->ID, '_wp_attachment_image_alt', true ); } if ( in_array( 'media_type', $fields, true ) ) { $data['media_type'] = wp_attachment_is_image( $post->ID ) ? 'image' : 'file'; } if ( in_array( 'mime_type', $fields, true ) ) { $data['mime_type'] = $post->post_mime_type; } if ( in_array( 'media_details', $fields, true ) ) { $data['media_details'] = wp_get_attachment_metadata( $post->ID ); // Ensure empty details is an empty object. if ( empty( $data['media_details'] ) ) { $data['media_details'] = new stdClass(); } elseif ( ! empty( $data['media_details']['sizes'] ) ) { foreach ( $data['media_details']['sizes'] as $size => &$size_data ) { if ( isset( $size_data['mime-type'] ) ) { $size_data['mime_type'] = $size_data['mime-type']; unset( $size_data['mime-type'] ); } // Use the same method image_downsize() does. $image_src = wp_get_attachment_image_src( $post->ID, $size ); if ( ! $image_src ) { continue; } $size_data['source_url'] = $image_src[0]; } $full_src = wp_get_attachment_image_src( $post->ID, 'full' ); if ( ! empty( $full_src ) ) { $data['media_details']['sizes']['full'] = array( 'file' => wp_basename( $full_src[0] ), 'width' => $full_src[1], 'height' => $full_src[2], 'mime_type' => $post->post_mime_type, 'source_url' => $full_src[0], ); } } else { $data['media_details']['sizes'] = new stdClass(); } } if ( in_array( 'post', $fields, true ) ) { $data['post'] = ! empty( $post->post_parent ) ? (int) $post->post_parent : null; } if ( in_array( 'source_url', $fields, true ) ) { $data['source_url'] = wp_get_attachment_url( $post->ID ); } if ( in_array( 'missing_image_sizes', $fields, true ) ) { require_once ABSPATH . 'wp-admin/includes/image.php'; $data['missing_image_sizes'] = array_keys( wp_get_missing_image_subsizes( $post->ID ) ); } $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; $data = $this->filter_response_by_context( $data, $context ); $links = $response->get_links(); // Wrap the data in a response object. $response = rest_ensure_response( $data ); foreach ( $links as $rel => $rel_links ) { foreach ( $rel_links as $link ) { $response->add_link( $rel, $link['href'], $link['attributes'] ); } } /** * Filters an attachment returned from the REST API. * * Allows modification of the attachment right before it is returned. * * @since 4.7.0 * * @param WP_REST_Response $response The response object. * @param WP_Post $post The original attachment post. * @param WP_REST_Request $request Request used to generate the response. */ return apply_filters( 'rest_prepare_attachment', $response, $post, $request ); } /** * Retrieves the attachment's schema, conforming to JSON Schema. * * @since 4.7.0 * * @return array Item schema as an array. */ public function get_item_schema() { if ( $this->schema ) { return $this->add_additional_fields_schema( $this->schema ); } $schema = parent::get_item_schema(); $schema['properties']['alt_text'] = array( 'description' => __( 'Alternative text to display when attachment is not displayed.' ), 'type' => 'string', 'context' => array( 'view', 'edit', 'embed' ), 'arg_options' => array( 'sanitize_callback' => 'sanitize_text_field', ), ); $schema['properties']['caption'] = array( 'description' => __( 'The attachment caption.' ), 'type' => 'object', 'context' => array( 'view', 'edit', 'embed' ), 'arg_options' => array( 'sanitize_callback' => null, // Note: sanitization implemented in self::prepare_item_for_database(). 'validate_callback' => null, // Note: validation implemented in self::prepare_item_for_database(). ), 'properties' => array( 'raw' => array( 'description' => __( 'Caption for the attachment, as it exists in the database.' ), 'type' => 'string', 'context' => array( 'edit' ), ), 'rendered' => array( 'description' => __( 'HTML caption for the attachment, transformed for display.' ), 'type' => 'string', 'context' => array( 'view', 'edit', 'embed' ), 'readonly' => true, ), ), ); $schema['properties']['description'] = array( 'description' => __( 'The attachment description.' ), 'type' => 'object', 'context' => array( 'view', 'edit' ), 'arg_options' => array( 'sanitize_callback' => null, // Note: sanitization implemented in self::prepare_item_for_database(). 'validate_callback' => null, // Note: validation implemented in self::prepare_item_for_database(). ), 'properties' => array( 'raw' => array( 'description' => __( 'Description for the attachment, as it exists in the database.' ), 'type' => 'string', 'context' => array( 'edit' ), ), 'rendered' => array( 'description' => __( 'HTML description for the attachment, transformed for display.' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), ), ); $schema['properties']['media_type'] = array( 'description' => __( 'Attachment type.' ), 'type' => 'string', 'enum' => array( 'image', 'file' ), 'context' => array( 'view', 'edit', 'embed' ), 'readonly' => true, ); $schema['properties']['mime_type'] = array( 'description' => __( 'The attachment MIME type.' ), 'type' => 'string', 'context' => array( 'view', 'edit', 'embed' ), 'readonly' => true, ); $schema['properties']['media_details'] = array( 'description' => __( 'Details about the media file, specific to its type.' ), 'type' => 'object', 'context' => array( 'view', 'edit', 'embed' ), 'readonly' => true, ); $schema['properties']['post'] = array( 'description' => __( 'The ID for the associated post of the attachment.' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), ); $schema['properties']['source_url'] = array( 'description' => __( 'URL to the original attachment file.' ), 'type' => 'string', 'format' => 'uri', 'context' => array( 'view', 'edit', 'embed' ), 'readonly' => true, ); $schema['properties']['missing_image_sizes'] = array( 'description' => __( 'List of the missing image sizes of the attachment.' ), 'type' => 'array', 'items' => array( 'type' => 'string' ), 'context' => array( 'edit' ), 'readonly' => true, ); unset( $schema['properties']['password'] ); $this->schema = $schema; return $this->add_additional_fields_schema( $this->schema ); } /** * Handles an upload via raw POST data. * * @since 4.7.0 * * @param string $data Supplied file data. * @param array $headers HTTP headers from the request. * @return array|WP_Error Data from wp_handle_sideload(). */ protected function upload_from_data( $data, $headers ) { if ( empty( $data ) ) { return new WP_Error( 'rest_upload_no_data', __( 'No data supplied.' ), array( 'status' => 400 ) ); } if ( empty( $headers['content_type'] ) ) { return new WP_Error( 'rest_upload_no_content_type', __( 'No Content-Type supplied.' ), array( 'status' => 400 ) ); } if ( empty( $headers['content_disposition'] ) ) { return new WP_Error( 'rest_upload_no_content_disposition', __( 'No Content-Disposition supplied.' ), array( 'status' => 400 ) ); } $filename = self::get_filename_from_disposition( $headers['content_disposition'] ); if ( empty( $filename ) ) { return new WP_Error( 'rest_upload_invalid_disposition', __( 'Invalid Content-Disposition supplied. Content-Disposition needs to be formatted as `attachment; filename="image.png"` or similar.' ), array( 'status' => 400 ) ); } if ( ! empty( $headers['content_md5'] ) ) { $content_md5 = array_shift( $headers['content_md5'] ); $expected = trim( $content_md5 ); $actual = md5( $data ); if ( $expected !== $actual ) { return new WP_Error( 'rest_upload_hash_mismatch', __( 'Content hash did not match expected.' ), array( 'status' => 412 ) ); } } // Get the content-type. $type = array_shift( $headers['content_type'] ); // Include filesystem functions to get access to wp_tempnam() and wp_handle_sideload(). require_once ABSPATH . 'wp-admin/includes/file.php'; // Save the file. $tmpfname = wp_tempnam( $filename ); $fp = fopen( $tmpfname, 'w+' ); if ( ! $fp ) { return new WP_Error( 'rest_upload_file_error', __( 'Could not open file handle.' ), array( 'status' => 500 ) ); } fwrite( $fp, $data ); fclose( $fp ); // Now, sideload it in. $file_data = array( 'error' => null, 'tmp_name' => $tmpfname, 'name' => $filename, 'type' => $type, ); $size_check = self::check_upload_size( $file_data ); if ( is_wp_error( $size_check ) ) { return $size_check; } $overrides = array( 'test_form' => false, ); $sideloaded = wp_handle_sideload( $file_data, $overrides ); if ( isset( $sideloaded['error'] ) ) { @unlink( $tmpfname ); return new WP_Error( 'rest_upload_sideload_error', $sideloaded['error'], array( 'status' => 500 ) ); } return $sideloaded; } /** * Parses filename from a Content-Disposition header value. * * As per RFC6266: * * content-disposition = "Content-Disposition" ":" * disposition-type *( ";" disposition-parm ) * * disposition-type = "inline" | "attachment" | disp-ext-type * ; case-insensitive * disp-ext-type = token * * disposition-parm = filename-parm | disp-ext-parm * * filename-parm = "filename" "=" value * | "filename*" "=" ext-value * * disp-ext-parm = token "=" value * | ext-token "=" ext-value * ext-token = * * @since 4.7.0 * * @link https://tools.ietf.org/html/rfc2388 * @link https://tools.ietf.org/html/rfc6266 * * @param string[] $disposition_header List of Content-Disposition header values. * @return string|null Filename if available, or null if not found. */ public static function get_filename_from_disposition( $disposition_header ) { // Get the filename. $filename = null; foreach ( $disposition_header as $value ) { $value = trim( $value ); if ( ! str_contains( $value, ';' ) ) { continue; } list( $type, $attr_parts ) = explode( ';', $value, 2 ); $attr_parts = explode( ';', $attr_parts ); $attributes = array(); foreach ( $attr_parts as $part ) { if ( ! str_contains( $part, '=' ) ) { continue; } list( $key, $value ) = explode( '=', $part, 2 ); $attributes[ trim( $key ) ] = trim( $value ); } if ( empty( $attributes['filename'] ) ) { continue; } $filename = trim( $attributes['filename'] ); // Unquote quoted filename, but after trimming. if ( str_starts_with( $filename, '"' ) && str_ends_with( $filename, '"' ) ) { $filename = substr( $filename, 1, -1 ); } } return $filename; } /** * Retrieves the query params for collections of attachments. * * @since 4.7.0 * * @return array Query parameters for the attachment collection as an array. */ public function get_collection_params() { $params = parent::get_collection_params(); $params['status']['default'] = 'inherit'; $params['status']['items']['enum'] = array( 'inherit', 'private', 'trash' ); $media_types = $this->get_media_types(); $params['media_type'] = array( 'default' => null, 'description' => __( 'Limit result set to attachments of a particular media type.' ), 'type' => 'string', 'enum' => array_keys( $media_types ), ); $params['mime_type'] = array( 'default' => null, 'description' => __( 'Limit result set to attachments of a particular MIME type.' ), 'type' => 'string', ); return $params; } /** * Handles an upload via multipart/form-data ($_FILES). * * @since 4.7.0 * * @param array $files Data from the `$_FILES` superglobal. * @param array $headers HTTP headers from the request. * @return array|WP_Error Data from wp_handle_upload(). */ protected function upload_from_file( $files, $headers ) { if ( empty( $files ) ) { return new WP_Error( 'rest_upload_no_data', __( 'No data supplied.' ), array( 'status' => 400 ) ); } // Verify hash, if given. if ( ! empty( $headers['content_md5'] ) ) { $content_md5 = array_shift( $headers['content_md5'] ); $expected = trim( $content_md5 ); $actual = md5_file( $files['file']['tmp_name'] ); if ( $expected !== $actual ) { return new WP_Error( 'rest_upload_hash_mismatch', __( 'Content hash did not match expected.' ), array( 'status' => 412 ) ); } } // Pass off to WP to handle the actual upload. $overrides = array( 'test_form' => false, ); // Bypasses is_uploaded_file() when running unit tests. if ( defined( 'DIR_TESTDATA' ) && DIR_TESTDATA ) { $overrides['action'] = 'wp_handle_mock_upload'; } $size_check = self::check_upload_size( $files['file'] ); if ( is_wp_error( $size_check ) ) { return $size_check; } // Include filesystem functions to get access to wp_handle_upload(). require_once ABSPATH . 'wp-admin/includes/file.php'; $file = wp_handle_upload( $files['file'], $overrides ); if ( isset( $file['error'] ) ) { return new WP_Error( 'rest_upload_unknown_error', $file['error'], array( 'status' => 500 ) ); } return $file; } /** * Retrieves the supported media types. * * Media types are considered the MIME type category. * * @since 4.7.0 * * @return array Array of supported media types. */ protected function get_media_types() { $media_types = array(); foreach ( get_allowed_mime_types() as $mime_type ) { $parts = explode( '/', $mime_type ); if ( ! isset( $media_types[ $parts[0] ] ) ) { $media_types[ $parts[0] ] = array(); } $media_types[ $parts[0] ][] = $mime_type; } return $media_types; } /** * Determine if uploaded file exceeds space quota on multisite. * * Replicates check_upload_size(). * * @since 4.9.8 * * @param array $file $_FILES array for a given file. * @return true|WP_Error True if can upload, error for errors. */ protected function check_upload_size( $file ) { if ( ! is_multisite() ) { return true; } if ( get_site_option( 'upload_space_check_disabled' ) ) { return true; } $space_left = get_upload_space_available(); $file_size = filesize( $file['tmp_name'] ); if ( $space_left < $file_size ) { return new WP_Error( 'rest_upload_limited_space', /* translators: %s: Required disk space in kilobytes. */ sprintf( __( 'Not enough space to upload. %s KB needed.' ), number_format( ( $file_size - $space_left ) / KB_IN_BYTES ) ), array( 'status' => 400 ) ); } if ( $file_size > ( KB_IN_BYTES * get_site_option( 'fileupload_maxk', 1500 ) ) ) { return new WP_Error( 'rest_upload_file_too_big', /* translators: %s: Maximum allowed file size in kilobytes. */ sprintf( __( 'This file is too big. Files must be less than %s KB in size.' ), get_site_option( 'fileupload_maxk', 1500 ) ), array( 'status' => 400 ) ); } // Include multisite admin functions to get access to upload_is_user_over_quota(). require_once ABSPATH . 'wp-admin/includes/ms.php'; if ( upload_is_user_over_quota( false ) ) { return new WP_Error( 'rest_upload_user_quota_exceeded', __( 'You have used your space quota. Please delete files before uploading.' ), array( 'status' => 400 ) ); } return true; } /** * Gets the request args for the edit item route. * * @since 5.5.0 * * @return array */ protected function get_edit_media_item_args() { return array( 'src' => array( 'description' => __( 'URL to the edited image file.' ), 'type' => 'string', 'format' => 'uri', 'required' => true, ), 'modifiers' => array( 'description' => __( 'Array of image edits.' ), 'type' => 'array', 'minItems' => 1, 'items' => array( 'description' => __( 'Image edit.' ), 'type' => 'object', 'required' => array( 'type', 'args', ), 'oneOf' => array( array( 'title' => __( 'Rotation' ), 'properties' => array( 'type' => array( 'description' => __( 'Rotation type.' ), 'type' => 'string', 'enum' => array( 'rotate' ), ), 'args' => array( 'description' => __( 'Rotation arguments.' ), 'type' => 'object', 'required' => array( 'angle', ), 'properties' => array( 'angle' => array( 'description' => __( 'Angle to rotate clockwise in degrees.' ), 'type' => 'number', ), ), ), ), ), array( 'title' => __( 'Crop' ), 'properties' => array( 'type' => array( 'description' => __( 'Crop type.' ), 'type' => 'string', 'enum' => array( 'crop' ), ), 'args' => array( 'description' => __( 'Crop arguments.' ), 'type' => 'object', 'required' => array( 'left', 'top', 'width', 'height', ), 'properties' => array( 'left' => array( 'description' => __( 'Horizontal position from the left to begin the crop as a percentage of the image width.' ), 'type' => 'number', ), 'top' => array( 'description' => __( 'Vertical position from the top to begin the crop as a percentage of the image height.' ), 'type' => 'number', ), 'width' => array( 'description' => __( 'Width of the crop as a percentage of the image width.' ), 'type' => 'number', ), 'height' => array( 'description' => __( 'Height of the crop as a percentage of the image height.' ), 'type' => 'number', ), ), ), ), ), ), ), ), 'rotation' => array( 'description' => __( 'The amount to rotate the image clockwise in degrees. DEPRECATED: Use `modifiers` instead.' ), 'type' => 'integer', 'minimum' => 0, 'exclusiveMinimum' => true, 'maximum' => 360, 'exclusiveMaximum' => true, ), 'x' => array( 'description' => __( 'As a percentage of the image, the x position to start the crop from. DEPRECATED: Use `modifiers` instead.' ), 'type' => 'number', 'minimum' => 0, 'maximum' => 100, ), 'y' => array( 'description' => __( 'As a percentage of the image, the y position to start the crop from. DEPRECATED: Use `modifiers` instead.' ), 'type' => 'number', 'minimum' => 0, 'maximum' => 100, ), 'width' => array( 'description' => __( 'As a percentage of the image, the width to crop the image to. DEPRECATED: Use `modifiers` instead.' ), 'type' => 'number', 'minimum' => 0, 'maximum' => 100, ), 'height' => array( 'description' => __( 'As a percentage of the image, the height to crop the image to. DEPRECATED: Use `modifiers` instead.' ), 'type' => 'number', 'minimum' => 0, 'maximum' => 100, ), )500-internal server error

Error occurred: 500 - internal server error