From 4d9845149f2322ef8e32e8bd4c2e782258e58fce Mon Sep 17 00:00:00 2001 From: Roberto Guagliardo Date: Sun, 1 Feb 2026 17:55:32 +0000 Subject: [PATCH] Refactor plugin structure: replace master-file.php with siti-google-shopping-feed.php and update release workflow paths --- .gitea/workflows/release.yml | 6 +- includes/SitiGoogleShoppingFeed.php | 561 ++++++++++++++++++ ...-file.php => siti-google-shopping-feed.php | 27 +- 3 files changed, 580 insertions(+), 14 deletions(-) create mode 100644 includes/SitiGoogleShoppingFeed.php rename master-file.php => siti-google-shopping-feed.php (56%) diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml index 587109b..56075b5 100644 --- a/.gitea/workflows/release.yml +++ b/.gitea/workflows/release.yml @@ -9,7 +9,7 @@ on: push: branches: [ main ] paths: - - 'master-file.php' + - 'siti-google-shopping-feed.php' - 'includes/**' - 'assets/**' - 'languages/**' @@ -18,8 +18,8 @@ jobs: release: uses: roberto/ci-workflows/.gitea/workflows/wp-plugin-release.yml@c6393ed47258d6f040ceeed3994b17b7faa3ef23 with: - main_file: master-file.php - slug: siti-plugin-template + main_file: siti-google-shopping-feed.php + slug: siti-google-shopping-feed release_body: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.release_notes || '' }} secrets: RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }} \ No newline at end of file diff --git a/includes/SitiGoogleShoppingFeed.php b/includes/SitiGoogleShoppingFeed.php new file mode 100644 index 0000000..3c99ed2 --- /dev/null +++ b/includes/SitiGoogleShoppingFeed.php @@ -0,0 +1,561 @@ +\n"; + echo "\n"; + echo "\n"; + self::output_tag( 'title', $site_name ); + self::output_tag( 'link', $site_url ); + self::output_tag( 'description', 'Google Shopping Feed' ); + + $page_size = (int) apply_filters( 'livebetter_google_feed_page_size', self::DEFAULT_PAGE_SIZE ); + $page_size = max( 1, $page_size ); + $page = 1; + + do { + $query_args = array( + 'status' => 'publish', + 'type' => array( 'simple', 'variation' ), + 'visibility' => array( 'visible', 'catalog', 'search' ), + 'limit' => $page_size, + 'page' => $page, + ); + + $query_args = apply_filters( 'livebetter_google_feed_query_args', $query_args, $page, $page_size ); + + $products = wc_get_products( $query_args ); + + foreach ( $products as $product ) { + self::render_item( $product ); + } + + $page++; + } while ( count( $products ) === $page_size ); + + echo "\n"; + echo "\n"; + } + + private static function render_item( WC_Product $product ) { + if ( 'publish' !== $product->get_status() ) { + return; + } + + $parent = null; + if ( $product->is_type( 'variation' ) ) { + $parent_id = $product->get_parent_id(); + $parent = $parent_id ? wc_get_product( $parent_id ) : null; + if ( $parent instanceof WC_Product && 'publish' !== $parent->get_status() ) { + return; + } + } + + $image_link = self::get_image_link( $product, $parent ); + if ( empty( $image_link ) ) { + return; + } + + if ( '' === $product->get_price() ) { + return; + } + + $price_data = self::get_price_data( $product ); + if ( empty( $price_data['price'] ) ) { + return; + } + + $availability = self::get_availability( $product ); + $brand = self::get_brand( $product, $parent ); + $gtin = self::get_gtin( $product, $parent ); + $mpn = self::get_mpn( $product, $parent ); + $gender = self::get_gender( $product, $parent ); + $age_group = self::get_age_group( $product, $parent ); + $size = self::get_size( $product, $parent ); + $size_system = $size ? self::get_size_system() : ''; + $size_type = $size ? self::get_size_type() : ''; + $color = self::get_color( $product, $parent ); + $material = self::get_material( $product, $parent ); + + $identifier_exists = ( empty( $brand ) && empty( $gtin ) && empty( $mpn ) ) ? 'no' : 'yes'; + + $item_data = array( + 'id' => (string) $product->get_id(), + 'item_group_id' => $parent ? (string) $parent->get_id() : '', + 'title' => self::get_title( $product, $parent ), + 'description' => self::get_description( $product, $parent ), + 'link' => $product->get_permalink(), + 'image_link' => $image_link, + 'availability' => $availability, + 'price' => $price_data['price'], + 'sale_price' => $price_data['sale_price'], + 'condition' => 'new', + 'brand' => $brand, + 'gtin' => $gtin, + 'mpn' => $mpn, + 'gender' => $gender, + 'age_group' => $age_group, + 'size' => $size, + 'size_system' => $size_system, + 'size_type' => $size_type, + 'color' => $color, + 'material' => $material, + 'identifier_exists' => $identifier_exists, + ); + + $item_data = apply_filters( 'livebetter_google_feed_item_data', $item_data, $product, $parent ); + + echo "\n"; + self::output_tag( 'g:id', $item_data['id'] ); + if ( ! empty( $item_data['item_group_id'] ) ) { + self::output_tag( 'g:item_group_id', $item_data['item_group_id'] ); + } + self::output_tag( 'title', $item_data['title'] ); + self::output_tag( 'description', $item_data['description'] ); + self::output_tag( 'link', $item_data['link'] ); + self::output_tag( 'g:image_link', $item_data['image_link'] ); + self::output_tag( 'g:availability', $item_data['availability'] ); + self::output_tag( 'g:price', $item_data['price'] ); + if ( ! empty( $item_data['sale_price'] ) ) { + self::output_tag( 'g:sale_price', $item_data['sale_price'] ); + } + self::output_tag( 'g:condition', $item_data['condition'] ); + if ( ! empty( $item_data['brand'] ) ) { + self::output_tag( 'g:brand', $item_data['brand'] ); + } + if ( ! empty( $item_data['gender'] ) ) { + self::output_tag( 'g:gender', $item_data['gender'] ); + } + if ( ! empty( $item_data['age_group'] ) ) { + self::output_tag( 'g:age_group', $item_data['age_group'] ); + } + if ( ! empty( $item_data['size'] ) ) { + self::output_tag( 'g:size', $item_data['size'] ); + } + if ( ! empty( $item_data['size_system'] ) ) { + self::output_tag( 'g:size_system', $item_data['size_system'] ); + } + if ( ! empty( $item_data['size_type'] ) ) { + self::output_tag( 'g:size_type', $item_data['size_type'] ); + } + if ( ! empty( $item_data['color'] ) ) { + self::output_tag( 'g:color', $item_data['color'] ); + } + if ( ! empty( $item_data['material'] ) ) { + self::output_tag( 'g:material', $item_data['material'] ); + } + if ( ! empty( $item_data['gtin'] ) ) { + self::output_tag( 'g:gtin', $item_data['gtin'] ); + } + if ( ! empty( $item_data['mpn'] ) ) { + self::output_tag( 'g:mpn', $item_data['mpn'] ); + } + if ( 'no' === $item_data['identifier_exists'] ) { + self::output_tag( 'g:identifier_exists', 'no' ); + } + echo "\n"; + } + + private static function get_title( WC_Product $product, $parent ) { + $title = $product->get_name(); + if ( empty( $title ) && $parent instanceof WC_Product ) { + $title = $parent->get_name(); + } + return $title; + } + + private static function get_description( WC_Product $product, $parent ) { + $description = $product->get_description(); + if ( empty( $description ) ) { + $description = $product->get_short_description(); + } + if ( empty( $description ) && $parent instanceof WC_Product ) { + $description = $parent->get_description(); + } + if ( empty( $description ) && $parent instanceof WC_Product ) { + $description = $parent->get_short_description(); + } + + $description = strip_shortcodes( (string) $description ); + $description = wp_strip_all_tags( $description ); + $description = preg_replace( '/\\s+/', ' ', $description ); + $description = trim( $description ); + + if ( function_exists( 'mb_substr' ) ) { + $description = mb_substr( $description, 0, 5000 ); + } else { + $description = substr( $description, 0, 5000 ); + } + + if ( '' === $description ) { + $description = self::get_title( $product, $parent ); + } + + return $description; + } + + private static function get_image_link( WC_Product $product, $parent ) { + $image_id = $product->get_image_id(); + if ( ! $image_id && $parent instanceof WC_Product ) { + $image_id = $parent->get_image_id(); + } + + if ( ! $image_id ) { + $gallery_ids = $product->get_gallery_image_ids(); + if ( empty( $gallery_ids ) && $parent instanceof WC_Product ) { + $gallery_ids = $parent->get_gallery_image_ids(); + } + $image_id = $gallery_ids ? (int) $gallery_ids[0] : 0; + } + + if ( ! $image_id ) { + return ''; + } + + $image_link = wp_get_attachment_image_url( $image_id, 'full' ); + + return $image_link ? $image_link : ''; + } + + private static function get_price_data( WC_Product $product ) { + $currency = get_woocommerce_currency(); + $decimals = wc_get_price_decimals(); + + $price = wc_get_price_to_display( $product ); + $price = wc_format_decimal( $price, $decimals ) . ' ' . $currency; + + $sale_price = ''; + if ( $product->is_on_sale() && '' !== $product->get_sale_price() ) { + $regular_price_value = wc_get_price_to_display( $product, array( 'price' => $product->get_regular_price() ) ); + $sale_price_value = wc_get_price_to_display( $product, array( 'price' => $product->get_sale_price() ) ); + $price = wc_format_decimal( $regular_price_value, $decimals ) . ' ' . $currency; + $sale_price = wc_format_decimal( $sale_price_value, $decimals ) . ' ' . $currency; + } + + return array( + 'price' => $price, + 'sale_price' => $sale_price, + ); + } + + private static function get_availability( WC_Product $product ) { + $stock_holder = self::get_stock_holder( $product ); + + if ( $stock_holder->managing_stock() ) { + $combined_stock = self::get_combined_stock( $stock_holder ); + if ( $combined_stock > 0 ) { + $availability = 'in_stock'; + } else { + $availability = 'out_of_stock'; + if ( $stock_holder->backorders_allowed() ) { + $availability = apply_filters( 'livebetter_google_feed_backorder_availability', $availability, $product, $stock_holder ); + } + } + } else { + $availability = $stock_holder->is_in_stock() ? 'in_stock' : 'out_of_stock'; + } + + return apply_filters( 'livebetter_google_feed_availability', $availability, $product, $stock_holder ); + } + + private static function get_stock_holder( WC_Product $product ) { + $managed_id = $product->get_stock_managed_by_id(); + if ( $managed_id && $managed_id !== $product->get_id() ) { + $holder = wc_get_product( $managed_id ); + if ( $holder instanceof WC_Product ) { + return $holder; + } + } + + return $product; + } + + private static function get_combined_stock( WC_Product $product ) { + $external_key = self::EXTERNAL_STOCK_META_KEY_FALLBACK; + if ( class_exists( 'Siti_Stock_Plugin' ) ) { + $external_key = Siti_Stock_Plugin::EXTERNAL_STOCK_META_KEY; + } + + $raw_stock = $product->get_stock_quantity( 'edit' ); + $base_stock = function_exists( 'wc_stock_amount' ) ? wc_stock_amount( $raw_stock ) : (int) $raw_stock; + $external_stock = (int) $product->get_meta( $external_key, true ); + + return max( 0, (int) $base_stock ) + max( 0, $external_stock ); + } + + private static function get_brand( WC_Product $product, $parent ) { + $brand = self::get_taxonomy_terms_value( $product, $parent, array( 'product_brand', 'product_brands', 'brands', 'brand' ) ); + + if ( empty( $brand ) ) { + $brand = $product->get_meta( '_wc_gla_brand', true ); + } + + if ( empty( $brand ) && $parent instanceof WC_Product ) { + $brand = $parent->get_meta( '_wc_gla_brand', true ); + } + + if ( empty( $brand ) ) { + $brand = self::get_attribute_value( $product, $parent, array( 'pa_merk', 'pa_brand', 'brand' ) ); + } + + return apply_filters( 'livebetter_google_feed_brand', $brand, $product, $parent ); + } + + private static function get_gtin( WC_Product $product, $parent ) { + $gtin = ''; + if ( method_exists( $product, 'get_global_unique_id' ) ) { + $gtin = preg_replace( '/[^0-9]/', '', (string) $product->get_global_unique_id() ); + } + if ( empty( $gtin ) && $parent instanceof WC_Product && method_exists( $parent, 'get_global_unique_id' ) ) { + $gtin = preg_replace( '/[^0-9]/', '', (string) $parent->get_global_unique_id() ); + } + + return apply_filters( 'livebetter_google_feed_gtin', $gtin, $product, $parent ); + } + + private static function get_mpn( WC_Product $product, $parent ) { + $mpn = $product->get_sku(); + if ( empty( $mpn ) && $parent instanceof WC_Product ) { + $mpn = $parent->get_sku(); + } + + return apply_filters( 'livebetter_google_feed_mpn', $mpn, $product, $parent ); + } + + private static function get_gender( WC_Product $product, $parent ) { + $gender = (string) $product->get_meta( self::GENDER_META_KEY, true ); + if ( empty( $gender ) && $parent instanceof WC_Product ) { + $gender = (string) $parent->get_meta( self::GENDER_META_KEY, true ); + } + + return apply_filters( 'livebetter_google_feed_gender', $gender, $product, $parent ); + } + + private static function get_age_group( WC_Product $product, $parent ) { + $age_group = (string) $product->get_meta( self::AGE_GROUP_META_KEY, true ); + if ( empty( $age_group ) && $parent instanceof WC_Product ) { + $age_group = (string) $parent->get_meta( self::AGE_GROUP_META_KEY, true ); + } + if ( empty( $age_group ) ) { + $age_group = 'adult'; + } + + return apply_filters( 'livebetter_google_feed_age_group', $age_group, $product, $parent ); + } + + private static function get_size( WC_Product $product, $parent ) { + $size = self::get_attribute_value( $product, $parent, array( 'pa_maat', 'maat' ) ); + + return apply_filters( 'livebetter_google_feed_size', $size, $product, $parent ); + } + + private static function get_size_system() { + return apply_filters( 'livebetter_google_feed_size_system', 'EU' ); + } + + private static function get_size_type() { + return apply_filters( 'livebetter_google_feed_size_type', 'regular' ); + } + + private static function get_color( WC_Product $product, $parent ) { + $color = self::get_attribute_value( $product, $parent, array( 'pa_kleur', 'kleur', 'color' ) ); + + return apply_filters( 'livebetter_google_feed_color', $color, $product, $parent ); + } + + private static function get_material( WC_Product $product, $parent ) { + $material = self::get_attribute_value( $product, $parent, array( 'pa_materiaal', 'materiaal', 'material' ) ); + + return apply_filters( 'livebetter_google_feed_material', $material, $product, $parent ); + } + + private static function get_attribute_value( WC_Product $product, $parent, array $keys ) { + foreach ( $keys as $key ) { + $value = trim( (string) $product->get_attribute( $key ) ); + if ( '' !== $value ) { + return $value; + } + } + + if ( $parent instanceof WC_Product ) { + foreach ( $keys as $key ) { + $value = trim( (string) $parent->get_attribute( $key ) ); + if ( '' !== $value ) { + return $value; + } + } + } + + return ''; + } + + private static function get_taxonomy_terms_value( WC_Product $product, $parent, array $taxonomies ) { + foreach ( $taxonomies as $taxonomy ) { + if ( ! taxonomy_exists( $taxonomy ) ) { + continue; + } + + $terms = get_the_terms( $product->get_id(), $taxonomy ); + if ( empty( $terms ) && $parent instanceof WC_Product ) { + $terms = get_the_terms( $parent->get_id(), $taxonomy ); + } + if ( $terms && ! is_wp_error( $terms ) ) { + return implode( ', ', wp_list_pluck( $terms, 'name' ) ); + } + } + + return ''; + } + + public static function register_product_data_tab( $tabs ) { + if ( ! is_array( $tabs ) ) { + return $tabs; + } + + $tabs['livebetter_google_shopping'] = array( + 'label' => __( 'Google Shopping', 'livebetter-google-shopping-feed' ), + 'target' => 'livebetter_google_shopping_data', + 'class' => array( 'show_if_simple', 'show_if_variable' ), + 'priority' => 80, + ); + + return $tabs; + } + + public static function render_product_data_panel() { + if ( ! function_exists( 'woocommerce_wp_select' ) ) { + return; + } + + global $post; + + if ( ! $post ) { + return; + } + + $product = wc_get_product( $post->ID ); + if ( ! $product instanceof WC_Product ) { + return; + } + + $gender = (string) $product->get_meta( self::GENDER_META_KEY, true ); + $age_group = (string) $product->get_meta( self::AGE_GROUP_META_KEY, true ); + if ( '' === $age_group ) { + $age_group = 'adult'; + } + + echo '
'; + + woocommerce_wp_select( + array( + 'id' => self::GENDER_META_KEY, + 'label' => __( 'Geslacht', 'livebetter-google-shopping-feed' ), + 'value' => $gender, + 'options' => array( + '' => __( 'Niet ingesteld', 'livebetter-google-shopping-feed' ), + 'male' => __( 'Male (M)', 'livebetter-google-shopping-feed' ), + 'female' => __( 'Female (F)', 'livebetter-google-shopping-feed' ), + 'unisex' => __( 'Unisex (U)', 'livebetter-google-shopping-feed' ), + ), + ) + ); + + woocommerce_wp_select( + array( + 'id' => self::AGE_GROUP_META_KEY, + 'label' => __( 'Doelgroep', 'livebetter-google-shopping-feed' ), + 'value' => $age_group, + 'options' => array( + 'adult' => __( '18+ (Adult)', 'livebetter-google-shopping-feed' ), + 'kids' => __( 'Kids', 'livebetter-google-shopping-feed' ), + 'toddler' => __( 'Toddler', 'livebetter-google-shopping-feed' ), + 'infant' => __( 'Infant', 'livebetter-google-shopping-feed' ), + 'newborn' => __( 'Newborn', 'livebetter-google-shopping-feed' ), + ), + ) + ); + + echo '
'; + } + + public static function save_product_data_fields( $post_id ) { + if ( ! isset( $_POST[ self::GENDER_META_KEY ] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing + return; + } + + $gender = sanitize_text_field( wp_unslash( $_POST[ self::GENDER_META_KEY ] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Missing + $age_group = sanitize_text_field( wp_unslash( $_POST[ self::AGE_GROUP_META_KEY ] ?? '' ) ); // phpcs:ignore WordPress.Security.NonceVerification.Missing + + update_post_meta( $post_id, self::GENDER_META_KEY, $gender ); + update_post_meta( $post_id, self::AGE_GROUP_META_KEY, $age_group ); + } + + private static function output_tag( $tag, $value ) { + $value = self::escape_xml( $value ); + echo '<' . $tag . '>' . $value . '\n"; + } + + private static function escape_xml( $value ) { + return htmlspecialchars( (string) $value, ENT_XML1 | ENT_COMPAT, 'UTF-8' ); + } +} diff --git a/master-file.php b/siti-google-shopping-feed.php similarity index 56% rename from master-file.php rename to siti-google-shopping-feed.php index ab2c274..6212507 100644 --- a/master-file.php +++ b/siti-google-shopping-feed.php @@ -1,6 +1,6 @@ set_owner( 'roberto' ); -$updater->set_repository( 'siti-plugin-template' ); -$updater->initialize(); \ No newline at end of file +$updater->set_repository( SITI_GOOGLE_SHOPPING_FEED_SLUG ); +$updater->initialize(); + +include_once plugin_dir_path( __FILE__ ) . 'includes/SitiGoogleShoppingFeed.php'; + +register_activation_hook( __FILE__, array( 'SitiGoogleShoppingFeed', 'activate' ) ); +register_deactivation_hook( __FILE__, array( 'SitiGoogleShoppingFeed', 'deactivate' ) ); + +SitiGoogleShoppingFeed::init(); \ No newline at end of file