File manager - Edit - /home/verseaumee/maze/wp-content/plugins/pinterest-for-woocommerce/src/ProductsXmlFeed.php
Back
<?php /** * Pinterest for WooCommerce Catalog Syncing * * @package Pinterest_For_WooCommerce/Classes/ * @version 1.0.0 */ namespace Automattic\WooCommerce\Pinterest; use Automattic\WooCommerce\Pinterest\Logger; use Automattic\WooCommerce\Pinterest\Product\Attributes\AttributeManager; use WC_Product; use WC_Product_Variable; use WC_Product_Variation; if ( ! defined( 'ABSPATH' ) ) { exit; } /** * Class adding Save Pin support. */ class ProductsXmlFeed { use PluginHelper; /** * The default data structure of the Item to be printed in the XML feed. * * @var array */ private static $feed_item_structure = array( 'g:id', 'item_group_id', 'title', 'description', 'g:product_type', 'link', 'g:image_link', 'g:availability', 'g:price', 'sale_price', 'g:mpn', 'g:brand', 'g:tax', 'g:shipping', 'g:additional_image_link', ); /** * Shipping object. Used for caching between calls to the shipping column function. * * @var Shipping|null $shipping */ private static $shipping = null; /** * Limit of characters allowed by Pinterest in the product description. * * @var int */ const DESCRIPTION_SIZE_CHARS_LIMIT = 10000; /** * Limit of additional images allowed by Pinterest. * * @var int */ const ADDITIONAL_IMAGES_LIMIT = 10; /** * Limit of categories allowed by Pinterest in the product_type. * * @var int */ const PRODUCT_TYPE_CATEGORIES_LIMIT = 5; /** * Limit of characters allowed by Pinterest in the product_type. * * @var int */ const PRODUCT_TYPE_CHARS_LIMIT = 1000; /** * Limit of characters allowed by Pinterest in the brand. * * @var int */ const BRAND_CHARS_LIMIT = 100; /** * Returns the XML header to be printed. * * @return string */ public static function get_xml_header() { return '<?xml version="1.0"?>' . PHP_EOL . '<rss version="2.0" xmlns:g="http://base.google.com/ns/1.0">' . PHP_EOL . "\t" . '<channel>' . PHP_EOL; } /** * Returns the XML footer to be printed. * * @return string */ public static function get_xml_footer() { return "\t" . '</channel>' . PHP_EOL . '</rss>'; } /** * Returns the Item's XML for the given product. * * @param WC_Product $product The product to print the XML for. * @param string $location The location to print the XML for. * @return string XML string. */ public static function get_xml_item( $product, $location ) { if ( ! self::is_product_fit_for_feed( $product ) ) { return ''; } $xml = "\t\t<item>" . PHP_EOL; /** * Filter that controls the attributes that will be added to the product XML file. * * @since 0.5.0 * @param array XML fields to add. * @param WC_Product Product for which the XML is being generated. */ foreach ( apply_filters( 'pinterest_for_woocommerce_feed_item_structure', self::$feed_item_structure, $product ) as $attribute ) { $method_name = 'get_property_' . str_replace( ':', '_', $attribute ); if ( method_exists( __CLASS__, $method_name ) ) { $att = call_user_func_array( array( __CLASS__, $method_name ), array( $product, $attribute ) ); $xml .= ! empty( $att ) ? "\t\t\t" . $att . PHP_EOL : ''; } } $xml .= self::get_attributes_xml( $product, "\t\t\t" ); $xml .= "\t\t</item>" . PHP_EOL; /** * Filter XML output for product * * @since 1.0.10 * @param string XML content. * @param WC_Product Product for which the XML is being generated. */ return apply_filters( 'pinterest_for_woocommerce_feed_item_xml', $xml, $product ); } /** * Helper method to return if a product is fit for the feed profile. * * @param WC_Product $product The product. * * @return boolean */ private static function is_product_fit_for_feed( $product ) { // Decide if product is fit for the feed based on price. $price = self::get_product_regular_price( $product ); if ( empty( $price ) || empty( floatval( $price ) ) ) { return false; } return true; } /** * Get the XML for all the product attributes. * Will only return the attributes which have been set * or are available for the product type. * * @param WC_Product $product WooCommerce product. * @param string $indent Line indentation string. * @return string XML string. */ private static function get_attributes_xml( $product, $indent ) { $attribute_manager = AttributeManager::instance(); $attributes = $attribute_manager->get_all_values( $product ); $xml = ''; // Merge with parent's attributes if it's a variation product. if ( $product instanceof WC_Product_Variation ) { $parent_product = wc_get_product( $product->get_parent_id() ); if ( $parent_product instanceof WC_Product ) { $parent_attributes = $attribute_manager->get_all_values( $parent_product ); $attributes = array_merge( $parent_attributes, $attributes ); } } foreach ( $attributes as $name => $value ) { $property = "g:{$name}"; $value = esc_xml( $value ); $xml .= "{$indent}<{$property}>{$value}</{$property}>" . PHP_EOL; } return $xml; } /** * Returns the Product ID. * * @param WC_Product $product the product. * @param string $property The name of the property. * * @return string */ private static function get_property_g_id( $product, $property ) { return '<' . $property . '>' . $product->get_id() . '</' . $property . '>'; } /** * Returns the item_group_id (parent id for variations). * * @param WC_Product $product the product. * @param string $property The name of the property. * * @return string */ private static function get_property_item_group_id( $product, $property ) { if ( ! $product->get_parent_id() ) { return; } return '<' . $property . '>' . $product->get_parent_id() . '</' . $property . '>'; } /** * Returns the product title. * * @param WC_Product $product the product. * @param string $property The name of the property. * * @return string */ private static function get_property_title( $product, $property ) { $title = wp_strip_all_tags( $product->get_name() ); return "<$property>" . self::sanitize( '<![CDATA[' . $title . ']]>' ) . "</$property>"; } /** * Returns the product description. * * @param WC_Product $product the product. * @param string $property The name of the property. * * @return string|void */ private static function get_property_description( $product, $property ) { // Guard against stale IDs or upstream lookup failures that pass a // non-product value through to the feed pipeline. if ( ! $product instanceof WC_Product ) { return; } if ( $product->get_parent_id() ) { $description = $product->get_description(); $parent = null; if ( empty( $description ) ) { /* * For variations without their own description, fall back to the parent * product's short description, then the parent's long description. * Avoid get_the_excerpt() here because WooCommerce stores the variation's * attribute summary in post_excerpt, which is not a useful description. * * wc_get_product() has its own per-request object cache, so sibling * variations of the same parent reuse the same load without * needing a second cache layer in this method. */ $parent = wc_get_product( $product->get_parent_id() ); if ( $parent ) { $description = $parent->get_short_description(); if ( empty( $description ) ) { $description = $parent->get_description(); } } } /** * Filters the description used for a variation product entry in the * Pinterest feed XML. Use this to override the variation→parent * fallback chain (e.g. emit a translated description, prefer parent * long over short, or inject a per-variation string). * * @since 1.4.27 * * @param string $description Resolved description after the variation→parent fallback. * @param WC_Product $product Variation product. * @param WC_Product|null $parent Parent product, or null when not loaded (variation had its own description, or the parent post was deleted). */ $description = apply_filters( 'pinterest_for_woocommerce_variation_description', $description, $product, $parent ); } else { $description = $product->get_short_description(); if ( empty( $description ) ) { $description = get_the_excerpt( $product->get_id() ); } } if ( empty( $description ) ) { return; } /** * Filters whether the shortcodes should be applied for product descriptions when generating the feed or be stripped out. * * @param bool $apply_shortcodes Shortcodes are applied if set to `true` and stripped out if set to `false`. * @param WC_Product $product WooCommerce product object. * * phpcs:disable WooCommerce.Commenting.CommentHooks.MissingSinceComment */ $apply_shortcodes = apply_filters( 'pinterest_for_woocommerce_product_description_apply_shortcodes', false, $product ); $description = self::strip_tags_from_string( $description, $apply_shortcodes ); // Limit the number of characters in the description to 10000. if ( strlen( $description ) > self::DESCRIPTION_SIZE_CHARS_LIMIT ) { /* translators: %s product id */ Logger::log( sprintf( esc_html__( 'The product [%s] has a description longer than the allowed limit.', 'pinterest-for-woocommerce' ), $product->get_id() ) ); } $description = substr( $description, 0, self::DESCRIPTION_SIZE_CHARS_LIMIT ); return "<$property>" . self::sanitize( '<![CDATA[' . $description . ']]>' ) . "</$property>"; } /** * Returns the product taxonomies. * * @param WC_Product $product the product. * @param string $property The name of the property. * * @return string */ private static function get_property_g_product_type( $product, $property ) { $id = $product->get_parent_id() ? $product->get_parent_id() : $product->get_id(); $taxonomies = array_map( self::class . '::sanitize', self::get_taxonomies( $id ) ); if ( empty( $taxonomies ) ) { return; } // Limit to first 5 categories as per Pinterest requirements. $original_count = count( $taxonomies ); if ( $original_count > self::PRODUCT_TYPE_CATEGORIES_LIMIT ) { $taxonomies = array_slice( $taxonomies, 0, self::PRODUCT_TYPE_CATEGORIES_LIMIT ); Logger::log( sprintf( 'Product [%1$s] has %2$d categories, limiting to first %3$d as per Pinterest requirements.', $id, $original_count, self::PRODUCT_TYPE_CATEGORIES_LIMIT ) ); } // Build product_type string. $product_type = implode( ' > ', $taxonomies ); // Ensure product_type doesn't exceed 1000 character limit. if ( strlen( $product_type ) > self::PRODUCT_TYPE_CHARS_LIMIT ) { Logger::log( sprintf( 'Product [%1$s] product_type length is %2$d characters, truncating to %3$d characters as per Pinterest requirements.', $id, strlen( $product_type ), self::PRODUCT_TYPE_CHARS_LIMIT ) ); // Build product_type by adding taxonomies until we hit the character limit, we always include the first taxonomy. $product_type = ''; foreach ( $taxonomies as $index => $taxonomy ) { $separator = $index > 0 ? ' > ' : ''; $new_product_type = $product_type . $separator . $taxonomy; if ( strlen( $new_product_type ) > self::PRODUCT_TYPE_CHARS_LIMIT && $index > 0 ) { break; } $product_type = $new_product_type; } } return '<' . $property . '>' . $product_type . '</' . $property . '>'; } /** * Returns the permalink. * * @since 1.4.16 Url has UTM parameters used for tracking. * * @param WC_Product $product the product. * @param string $property The name of the property. * * @return string */ private static function get_property_link( $product, $property ) { $product_url = $product->get_permalink(); $product_url_with_utm = self::add_utm_parameters( $product_url ); return '<' . $property . '><![CDATA[' . $product_url_with_utm . ']]></' . $property . '>'; } /** * Add UTM parameters to the product URL. * This is used to track the performance of the product pins. * The parameters are: * - utm_source: pinterest * - utm_medium: social * * @since 1.4.16 * * @param string $product_url The product URL. * @return string */ private static function add_utm_parameters( $product_url ) { $utm_params = array( 'utm_source' => 'pinterest', 'utm_medium' => 'social', ); return add_query_arg( $utm_params, $product_url ); } /** * Returns the URL of the main product image. * * @param WC_Product $product the product. * @param string $property The name of the property. * * @return string */ private static function get_property_g_image_link( $product, $property ) { $image_id = $product->get_image_id(); if ( ! $image_id ) { return ''; } /** * Get the image with a filter for default size. */ $image = wp_get_attachment_image_src( $image_id, apply_filters( 'pinterest_for_woocommerce_feed_image_size', 'full' ) ); if ( ! $image ) { return; } return '<' . $property . '><![CDATA[' . $image[0] . ']]></' . $property . '>'; } /** * Returns the availability of the product. * * @param WC_Product $product the product. * @param string $property The name of the property. * * @return string */ private static function get_property_g_availability( $product, $property ) { switch ( $product->get_stock_status() ) { case 'instock': $stock_status = 'in stock'; break; case 'outofstock': $stock_status = 'out of stock'; break; case 'onbackorder': $stock_status = 'preorder'; break; default: $stock_status = $product->get_stock_status(); break; } return '<' . $property . '>' . $stock_status . '</' . $property . '>'; } /** * Returns the base price, or the min base price for a variable product. * * @param WC_Product $product the product. * @param string $property The name of the property. * * @return string */ private static function get_property_g_price( $product, $property ) { $price = self::get_product_regular_price( $product ); if ( empty( $price ) ) { return; } return '<' . $property . '>' . wc_format_decimal( $price, self::get_currency_decimals() ) . get_woocommerce_currency() . '</' . $property . '>'; } /** * Returns the sale price of the product, or the min sale price for a variable product. * * @param WC_Product $product the product. * @param string $property The name of the property. * * @return string */ private static function get_property_sale_price( $product, $property ) { if ( ! $product->get_parent_id() && method_exists( $product, 'get_variation_sale_price' ) ) { $regular_price = $product->get_variation_regular_price( 'min', true ); $sale_price = $product->get_variation_sale_price( 'min', true ); $price = $regular_price > $sale_price ? $sale_price : false; } else { $sale_price = $product->get_sale_price(); $price = $sale_price ? wc_get_price_to_display( $product, array( 'price' => $sale_price, ) ) : ''; } if ( empty( $price ) ) { return; } return '<' . $property . '>' . wc_format_decimal( $price, self::get_currency_decimals() ) . get_woocommerce_currency() . '</' . $property . '>'; } /** * Returns the SKU in order to populate the MPN field. * * @param WC_Product $product the product. * @param string $property The name of the property. * * @return string */ private static function get_property_g_mpn( $product, $property ) { return '<' . $property . '>' . self::sanitize( $product->get_sku() ) . '</' . $property . '>'; } /** * Returns the brand of the product from the core WooCommerce product_brand taxonomy. * * @param WC_Product $product The product. * @param string $property The name of the property. * * @return string */ private static function get_property_g_brand( $product, $property ) { $id = $product->get_parent_id() ? $product->get_parent_id() : $product->get_id(); $terms = wc_get_object_terms( $id, 'product_brand' ); if ( empty( $terms ) || is_wp_error( $terms ) ) { return ''; } /** * Filters the brand value included in the Pinterest feed. * * @since 1.4.27 * @param string $brand The brand name. * @param WC_Product $product The product. */ $brand = apply_filters( 'pinterest_for_woocommerce_product_brand', $terms[0]->name, $product ); if ( empty( $brand ) ) { return ''; } // Ensure brand doesn't exceed Pinterest's character limit. if ( strlen( $brand ) > self::BRAND_CHARS_LIMIT ) { Logger::log( sprintf( 'Product [%1$s] brand length is %2$d characters, truncating to %3$d characters as per Pinterest requirements.', $id, strlen( $brand ), self::BRAND_CHARS_LIMIT ) ); $brand = substr( $brand, 0, self::BRAND_CHARS_LIMIT ); } return '<' . $property . '>' . self::sanitize( $brand ) . '</' . $property . '>'; } /** * Returns the gallery images for the product. * * @param WC_Product $product the product. * @param string $property The name of the property. * * @return string */ private static function get_property_g_additional_image_link( $product, $property ) { $attachment_ids = $product->get_gallery_image_ids(); $images = array(); if ( $attachment_ids && $product->get_image_id() ) { foreach ( $attachment_ids as $attachment_id ) { /** * Get the image with a filter for default size. */ $image = wp_get_attachment_image_src( $attachment_id, apply_filters( 'pinterest_for_woocommerce_feed_image_size', 'full' ) ); $images[] = $image ? $image[0] : false; } } if ( empty( $images ) ) { return; } $images = array_slice( $images, 0, self::ADDITIONAL_IMAGES_LIMIT ); $images = implode( ',', $images ); return '<' . $property . '><![CDATA[' . $images . ']]></' . $property . '>'; } /** * Returns the product shipping information. * * @since 1.0.5 * * @param WC_Product $product The product. * @param string $property The name of the property. * @return string */ private static function get_property_g_shipping( $product, $property ) { $currency = get_woocommerce_currency(); $shipping = self::get_shipping(); $shipping_info = $shipping->prepare_shipping_info( $product ); if ( empty( $shipping_info ) ) { return ''; } $shipping_nodes = array(); /* * Entry is a one or multiple XML nodes in the following format: * <g:shipping> * <g:country>...</g:country> * <g:region>...</g:region> * <g:service>...</g:service> * <g:price>...</g:price> * </g:shipping> */ foreach ( $shipping_info as $info ) { $shipping_name = self::sanitize( $info['name'] ); $shipping_nodes[] = '<g:shipping>' . PHP_EOL . "\t\t\t\t<g:country>$info[country]</g:country>" . PHP_EOL . ( $info['state'] ? "\t\t\t\t<g:region>$info[state]</g:region>" . PHP_EOL : '' ) . "\t\t\t\t<g:service>$shipping_name</g:service>" . PHP_EOL . "\t\t\t\t<g:price>$info[cost] $currency</g:price>" . PHP_EOL . "\t\t\t</g:shipping>"; } return implode( PHP_EOL . "\t\t\t", $shipping_nodes ); } /** * Helper method to return the taxonomies of the product in a useful format. * * @param integer $product_id The product ID. * * @return array */ private static function get_taxonomies( $product_id ) { $terms = wc_get_object_terms( $product_id, 'product_cat' ); if ( empty( $terms ) ) { return array(); } return wp_list_pluck( $terms, 'name' ); } /** * Get locale currency decimals */ private static function get_currency_decimals() { $currencies = get_transient( PINTEREST_FOR_WOOCOMMERCE_PREFIX . '_currencies_list' ); if ( ! $currencies ) { $locale_info = include WC()->plugin_path() . '/i18n/locale-info.php'; $currencies = wp_list_pluck( $locale_info, 'num_decimals', 'currency_code' ); set_transient( PINTEREST_FOR_WOOCOMMERCE_PREFIX . '_currencies_list', $currencies, DAY_IN_SECONDS ); } return $currencies[ get_woocommerce_currency() ] ?? 2; } /** Fetch shipping object. * * @since 1.0.5 * * @return Shipping */ private static function get_shipping() { if ( null === self::$shipping ) { self::$shipping = new Shipping(); /** * When we start generating lets make sure that the cart is loaded. * Various shipping and tax functions are using elements of cart. */ wc_load_cart(); } return self::$shipping; } /** * Helper method to return the regular price of a product. * * @param WC_Product|WC_Product_Variable $product The product. * * @return string */ private static function get_product_regular_price( $product ) { if ( ! $product->get_parent_id() && method_exists( $product, 'get_variation_price' ) ) { $price = $product->get_variation_regular_price( 'min', true ); } else { $price = wc_get_price_to_display( $product, array( 'price' => $product->get_regular_price(), ) ); } return $price; } /** * Sanitize XML. * After this method the string should be a valid XML string to fit inside * a XML tag directly. If a CDATA markup is used it also needs to be passed * along the string. * * This operation consist of two steps: * * 1. First a standardized esc_xml WordPress method is used. * This escapes XML control characters inside the text block. * * 2. Remove all UTF-8 characters that are not part of the XML specification. * We search the whole string and remove the not-allowed chars. Since XML * does not understand them removing is the only operation that we can do * that will produce a valid XML. * For information about allowed UTF-8 characters please go to * https://www.w3.org/TR/xml/ documentation, section charsets. * * @since 1.0.9 * @param string $xml_fragment XML fragment for sanitization. * @return string Sanitized XML fragment. */ private static function sanitize( $xml_fragment ) { return esc_xml( preg_replace( '/[^\x{9}\x{A}\x{D}\x{20}-\x{D7FF}\x{E000}-\x{FFFD}\x{10000}-\x{10FFFF}]/u', ' ', $xml_fragment ) ); } }
| ver. 1.4 |
Github
|
.
| PHP 8.5.7 | Generation time: 0 |
proxy
|
phpinfo
|
Settings