Add product query support for Stock indicator block (https://github.com/woocommerce/woocommerce-blocks/pull/7734)

* Add product query support for Stock indicator block

On the client side, when the Stock indicator block is used within the product query block, the markup will be rendered on the server side - No javascript related to Stock indicator block will be rendered.

* Escape all values in output string

Whenever we are rendering data, we should escape it. Escaping output prevents XSS (Cross-site scripting) attacks.

* Change $is_on_backorder type & escape just before printing

For more info:
https://github.com/woocommerce/woocommerce-blocks/pull/7734#discussion_r1035971939
https://github.com/woocommerce/woocommerce-blocks/pull/7734#discussion_r1035975712
This commit is contained in:
Manish Menaria 2022-12-01 17:14:05 +05:30 committed by GitHub
parent 9f3d61e1ea
commit 0d851fdb29
5 changed files with 128 additions and 17 deletions

View File

@ -1,15 +1,23 @@
interface BlockAttributes { /**
productId: { * Internal dependencies
type: string; */
default: number; import { BlockAttributes } from './types';
};
}
export const blockAttributes: BlockAttributes = { export const blockAttributes: Record<
keyof BlockAttributes,
{
type: string;
default: unknown;
}
> = {
productId: { productId: {
type: 'number', type: 'number',
default: 0, default: 0,
}, },
isDescendentOfQueryLoop: {
type: 'boolean',
default: false,
},
}; };
export default blockAttributes; export default blockAttributes;

View File

@ -3,6 +3,9 @@
*/ */
import EditProductLink from '@woocommerce/editor-components/edit-product-link'; import EditProductLink from '@woocommerce/editor-components/edit-product-link';
import { useBlockProps } from '@wordpress/block-editor'; import { useBlockProps } from '@wordpress/block-editor';
import type { BlockEditProps } from '@wordpress/blocks';
import { ProductQueryContext as Context } from '@woocommerce/blocks/product-query/types';
import { useEffect } from 'react';
/** /**
* Internal dependencies * Internal dependencies
@ -16,16 +19,28 @@ import {
} from './constants'; } from './constants';
import type { BlockAttributes } from './types'; import type { BlockAttributes } from './types';
interface Props { const Edit = ( {
attributes: BlockAttributes; attributes,
} setAttributes,
context,
const Edit = ( { attributes }: Props ): JSX.Element => { }: BlockEditProps< BlockAttributes > & { context: Context } ): JSX.Element => {
const blockProps = useBlockProps(); const blockProps = useBlockProps();
const blockAttrs = {
...attributes,
...context,
};
const isDescendentOfQueryLoop = Number.isFinite( context.queryId );
useEffect(
() => setAttributes( { isDescendentOfQueryLoop } ),
[ setAttributes, isDescendentOfQueryLoop ]
);
return ( return (
<div { ...blockProps }> <div { ...blockProps }>
<EditProductLink /> <EditProductLink />
<Block { ...attributes } /> <Block { ...blockAttrs } />
</div> </div>
); );
}; };

View File

@ -10,7 +10,6 @@ import type { BlockConfiguration } from '@wordpress/blocks';
import sharedConfig from '../shared/config'; import sharedConfig from '../shared/config';
import attributes from './attributes'; import attributes from './attributes';
import edit from './edit'; import edit from './edit';
import { Save } from './save';
import { supports } from './supports'; import { supports } from './supports';
import { import {
@ -28,7 +27,12 @@ const blockConfig: BlockConfiguration = {
attributes, attributes,
supports, supports,
edit, edit,
save: Save, usesContext: [ 'query', 'queryId', 'postId' ],
ancestor: [
'@woocommerce/all-products',
'@woocommerce/single-product',
'core/post-template',
],
}; };
registerExperimentalBlockType( 'woocommerce/product-stock-indicator', { registerExperimentalBlockType( 'woocommerce/product-stock-indicator', {

View File

@ -1,3 +1,4 @@
export interface BlockAttributes { export interface BlockAttributes {
productId: number; productId: number;
isDescendentOfQueryLoop: boolean;
} }

View File

@ -1,6 +1,8 @@
<?php <?php
namespace Automattic\WooCommerce\Blocks\BlockTypes; namespace Automattic\WooCommerce\Blocks\BlockTypes;
use Automattic\WooCommerce\Blocks\Utils\StyleAttributesUtils;
/** /**
* ProductStockIndicator class. * ProductStockIndicator class.
*/ */
@ -48,7 +50,88 @@ class ProductStockIndicator extends AbstractBlock {
* This registers the scripts; it does not enqueue them. * This registers the scripts; it does not enqueue them.
*/ */
protected function register_block_type_assets() { protected function register_block_type_assets() {
parent::register_block_type_assets(); return null;
$this->register_chunk_translations( [ $this->block_name ] ); }
/**
* Register the context.
*/
protected function get_block_type_uses_context() {
return [ 'query', 'queryId', 'postId' ];
}
/**
* Get stock text based on stock. For example:
* - In stock
* - Out of stock
* - Available on backorder
* - 2 left in stock
*
* @param [bool] $is_in_stock Whether the product is in stock.
* @param [bool] $is_low_stock Whether the product is low in stock.
* @param [int|null] $low_stock_amount The amount of stock that is considered low.
* @param [bool] $is_on_backorder Whether the product is on backorder.
* @return string Stock text.
*/
protected static function getTextBasedOnStock( $is_in_stock, $is_low_stock, $low_stock_amount, $is_on_backorder ) {
if ( $is_low_stock ) {
return sprintf(
/* translators: %d is number of items in stock for product */
__( '%d left in stock', 'woo-gutenberg-products-block' ),
$low_stock_amount
);
} elseif ( $is_on_backorder ) {
return __( 'Available on backorder', 'woo-gutenberg-products-block' );
} elseif ( $is_in_stock ) {
return __( 'In stock', 'woo-gutenberg-products-block' );
} else {
return __( 'Out of stock', 'woo-gutenberg-products-block' );
}
}
/**
* Include and render the block.
*
* @param array $attributes Block attributes. Default empty array.
* @param string $content Block content. Default empty string.
* @param WP_Block $block Block instance.
* @return string Rendered block type output.
*/
protected function render( $attributes, $content, $block ) {
if ( ! empty( $content ) ) {
parent::register_block_type_assets();
$this->register_chunk_translations( [ $this->block_name ] );
return $content;
}
$post_id = $block->context['postId'];
$product = wc_get_product( $post_id );
$is_in_stock = $product->is_in_stock();
$is_on_backorder = $product->is_on_backorder();
$low_stock_amount = $product->get_low_stock_amount();
$total_stock = $product->get_stock_quantity();
$is_low_stock = $low_stock_amount && $total_stock <= $low_stock_amount;
$classes_and_styles = StyleAttributesUtils::get_classes_and_styles_by_attributes( $attributes );
$classnames = isset( $classes_and_styles['classes'] ) ? ' ' . $classes_and_styles['classes'] . ' ' : '';
$classnames .= isset( $attributes['className'] ) ? ' ' . $attributes['className'] . ' ' : '';
$classnames .= ! $is_in_stock ? ' wc-block-components-product-stock-indicator--out-of-stock ' : '';
$classnames .= $is_in_stock ? ' wc-block-components-product-stock-indicator--in-stock ' : '';
$classnames .= $is_low_stock ? ' wc-block-components-product-stock-indicator--low-stock ' : '';
$classnames .= $is_on_backorder ? ' wc-block-components-product-stock-indicator--available-on-backorder ' : '';
$output = '';
$output .= '<div class="wc-block-components-product-stock-indicator ' . esc_attr( $classnames ) . '"';
$output .= isset( $classes_and_styles['styles'] ) ? ' style="' . esc_attr( $classes_and_styles['styles'] ) . '"' : '';
$output .= '>';
$output .= wp_kses_post( self::getTextBasedOnStock( $is_in_stock, $is_low_stock, $low_stock_amount, $is_on_backorder ) );
$output .= '</div>';
return $output;
} }
} }