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 . '' . $tag . ">\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