diff --git a/README.md b/README.md index 07d0f0c..ce2a862 100644 --- a/README.md +++ b/README.md @@ -5,11 +5,11 @@ **Requires at least:** 5.1 **Tested up to:** 6.6.1 **Requires PHP:** 7.0 -**Stable tag:** 0.1.1 +**Stable tag:** 0.2.0 **License:** GPLv2 or later **License URI:** https://www.gnu.org/licenses/gpl-2.0.html -A beautiful way to display products from affiliate network product feeds on your website. Currently supports TradeTracker and AdTraction. +A beautiful way to display products from affiliate network product feeds on your website. Currently supports Daisycon, TradeTracker and AdTraction. ## Description ## @@ -19,6 +19,7 @@ products from product feeds of various affiliate networks. Networks currently supported: * AdTraction * TradeTracker +* Daisycon **Features:** @@ -83,6 +84,12 @@ Colors can be adjusted by overriding the default CSS variables: ## Changelog ## +### 0.2.0 ### +* Added: Uninstall function +* Added: Daisycon support +* Added: Shortcode for direct links to products +* Improved: SQL logic + ### 0.1.1 ### * Added: Product ID and Feed in selection section * Added: AdTraction sale price diff --git a/affiliate-product-highlights.php b/affiliate-product-highlights.php index 610be6c..5f2ef7b 100644 --- a/affiliate-product-highlights.php +++ b/affiliate-product-highlights.php @@ -7,7 +7,7 @@ * Author URI: https://koenreus.com * Text Domain: affiliate-product-highlights * Domain Path: /languages - * Version: 0.1.1 + * Version: 0.2.0 * * @package Affiliate_Product_Highlights */ @@ -18,6 +18,7 @@ register_activation_hook(__FILE__, ['Koen12344\AffiliateProductHighlights\Plugin', 'activate']); register_deactivation_hook(__FILE__, ['Koen12344\AffiliateProductHighlights\Plugin', 'deactivate']); +register_uninstall_hook(__FILE__, ['Koen12344\AffiliateProductHighlights\Plugin', 'uninstall']); $affiliate_product_highlights = new Plugin(__FILE__); diff --git a/readme.txt b/readme.txt index 8de6e21..0a2d64b 100644 --- a/readme.txt +++ b/readme.txt @@ -5,11 +5,11 @@ Tags: tradetracker, adtraction, affiliate, feed, products Requires at least: 5.1 Tested up to: 6.6.1 Requires PHP: 7.0 -Stable tag: 0.1.1 +Stable tag: 0.2.0 License: GPLv2 or later License URI: https://www.gnu.org/licenses/gpl-2.0.html -A beautiful way to display products from affiliate network product feeds on your website. Currently supports TradeTracker and AdTraction. +A beautiful way to display products from affiliate network product feeds on your website. Currently supports Daisycon, TradeTracker and AdTraction. == Description == @@ -19,6 +19,7 @@ products from product feeds of various affiliate networks. Networks currently supported: * AdTraction * TradeTracker +* Daisycon **Features:** @@ -79,6 +80,12 @@ Colors can be adjusted by overriding the default CSS variables: == Changelog == += 0.2.0 = +* Added: Uninstall function +* Added: Daisycon support +* Added: Shortcode for direct links to products +* Improved: SQL logic + = 0.1.1 = * Added: Product ID and Feed in selection section * Added: AdTraction sale price diff --git a/src/php/BackgroundProcessing/BackgroundProcess.php b/src/php/BackgroundProcessing/BackgroundProcess.php index fb0f2b2..f13ca01 100644 --- a/src/php/BackgroundProcessing/BackgroundProcess.php +++ b/src/php/BackgroundProcessing/BackgroundProcess.php @@ -2,7 +2,11 @@ namespace Koen12344\AffiliateProductHighlights\BackgroundProcessing; -use Koen12344\AffiliateProductHighlights\Provider\AdTraction\ProductMapping; +use Exception; +use InvalidArgumentException; +use Koen12344\AffiliateProductHighlights\Provider\AdTraction\ProductMapping as AdtractionProductMapping; +use Koen12344\AffiliateProductHighlights\Provider\Daisycon\ProductMapping as DaisyconProductMapping; +use Koen12344\AffiliateProductHighlights\Provider\TradeTracker\ProductMapping as TradeTrackerProductMapping; use SimpleXMLElement; use WP_Http; use XMLReader; @@ -55,7 +59,7 @@ public function import_images(int $feed_id, int $product_id, $images){ public function download_xml_file($url) { if (filter_var($url, FILTER_VALIDATE_URL) === false) { - return false; + throw new InvalidArgumentException(__('Invalid feed URL', 'affiliate-product-highlights')); } if(!function_exists('wp_tempnam')){ @@ -66,7 +70,7 @@ public function download_xml_file($url) { $handle = fopen($tmp_file, 'w'); if ($handle === false) { - return false; + throw new Exception(__('Unable to to get write access to store temporary file.', 'affiliate-product-highlights')); } $http = new WP_Http(); @@ -79,7 +83,7 @@ public function download_xml_file($url) { if (is_wp_error($response)) { @fclose($handle); @unlink($tmp_file); - return false; + throw new Exception(sprintf(__('Unable to download the feed: %s', 'affiliate-product-highlights'), $response->get_error_message())); } fclose($handle); @@ -93,7 +97,7 @@ public function import_xml($file, int $feed_id, $feed_type) { $reader->open($file); while ($reader->read()) { - if ($reader->nodeType == XMLReader::ELEMENT && $reader->name == 'product') { + if ($reader->nodeType == XMLReader::ELEMENT && ($reader->name === 'product' || $reader->name === 'product_info')) { $product = new SimpleXMLElement($reader->readOuterXML()); if($feed_type == 'tradetracker'){ @@ -103,14 +107,21 @@ public function import_xml($file, int $feed_id, $feed_type) { (int)$product->campaignID, $feed_id )); - $mapped_product = new \Koen12344\AffiliateProductHighlights\Provider\TradeTracker\ProductMapping($product); + $mapped_product = new TradeTrackerProductMapping($product); }elseif($feed_type == 'adtraction'){ $existing_product = $wpdb->get_row($wpdb->prepare( "SELECT * FROM " . $wpdb->prefix . 'phft_products' . " WHERE sku = %s AND feed_id = %d", (string)$product->SKU, $feed_id )); - $mapped_product = new ProductMapping($product); + $mapped_product = new AdtractionProductMapping($product); + }elseif($feed_type == 'daisycon'){ + $existing_product = $wpdb->get_row($wpdb->prepare( + "SELECT * FROM " . $wpdb->prefix . 'phft_products' . " WHERE sku = %s AND feed_id = %d", + (string)$product->sku, + $feed_id + )); + $mapped_product = new DaisyconProductMapping($product); }else{ break; } @@ -132,29 +143,30 @@ public function import_xml($file, int $feed_id, $feed_type) { } else { $product_slug = sanitize_title($product_data['product_name']); $product_data['slug'] = $product_slug; - $inserted = false; $suffix = 1; - while(!$inserted){ + while($suffix <= 20){ //Try 20 times then bail $result = @$wpdb->insert($wpdb->prefix . 'phft_products', $product_data); - if($result){ - $inserted = true; - }else{ - if($wpdb->last_error && strpos($wpdb->last_error, 'Duplicate entry') !== false){ - $product_data['slug'] = $product_slug . '-' . $suffix; - $suffix++; - }else{ - break; - } + if($result) { + break; + } + + if($wpdb->last_error && strpos($wpdb->last_error, "for key 'slug_unique'") !== false){ + $product_data['slug'] = $product_slug . '-' . $suffix; + $suffix++; + continue; } + break; } $inserted_id = $wpdb->insert_id; } + if($inserted_id > 0){ + $this->import_images($feed_id, (int)$inserted_id, $mapped_product->get_product_images()); + } - $this->import_images($feed_id, (int)$inserted_id, $mapped_product->get_product_images()); } } @@ -174,7 +186,7 @@ private function split_feed($item){ $output_xml = new SimpleXMLElement(''); while($reader->read()){ - if ($reader->nodeType == XMLReader::ELEMENT && $reader->name === 'product') { + if ($reader->nodeType == XMLReader::ELEMENT && ($reader->name === 'product' || $reader->name === 'product_info')) { $product = new SimpleXMLElement($reader->readOuterXML()); $node = dom_import_simplexml($output_xml->addChild('product')); $node->parentNode->replaceChild($node->ownerDocument->importNode(dom_import_simplexml($product), true), $node); @@ -223,14 +235,25 @@ private function download_feed($item){ $xml_url = get_post_meta($item['feed_id'], '_phft_feed_url', true); - $temp_file = $this->download_xml_file($xml_url); + $network = $this->get_affiliate_network($xml_url); + if(!$network){ + update_post_meta($item['feed_id'], '_phft_last_error', __('The affiliate network this feed belongs to is unrecognized')); + return false; + } - update_post_meta($item['feed_id'], '_phft_last_import', time()); + try{ + $temp_file = $this->download_xml_file($xml_url); + }catch (Exception $e){ + update_post_meta($item['feed_id'], '_phft_last_error', $e->getMessage()); + return false; + } $item['action'] = 'split_feed'; - $item['feed_type'] = $this->get_affiliate_network($xml_url); + $item['feed_type'] = $network; $item['temp_file'] = $temp_file; + update_post_meta($item['feed_id'], '_phft_last_import', time()); + return $item; } @@ -249,8 +272,9 @@ private function get_affiliate_network($url) { $networks = [ - 'adtraction' => 'adtraction.com', - 'tradetracker' => 'tradetracker.net', + 'adtraction' => 'adtraction.com', + 'tradetracker' => 'tradetracker.net', + 'daisycon' => 'daisycon.io', ]; @@ -260,6 +284,6 @@ private function get_affiliate_network($url) { } } - return 'unknown'; + return false; } } diff --git a/src/php/Metabox/FeedMetabox.php b/src/php/Metabox/FeedMetabox.php index 82fba06..4fe7bc7 100644 --- a/src/php/Metabox/FeedMetabox.php +++ b/src/php/Metabox/FeedMetabox.php @@ -17,10 +17,16 @@ public function get_title(): string { } public function render(WP_Post $post) { + + $last_error = get_post_meta($post->ID, '_phft_last_error', true); $feed_url = get_post_meta($post->ID, '_phft_feed_url', true); $value = $feed_url ?: ''; + if(!empty($last_error)){ + echo sprintf(__('Last error: %s', 'affiliate-product-highlights'), $last_error); + } + echo "Feed Url: "; } } diff --git a/src/php/Plugin.php b/src/php/Plugin.php index 65fc434..2ad02dd 100644 --- a/src/php/Plugin.php +++ b/src/php/Plugin.php @@ -15,7 +15,7 @@ class Plugin { const DOMAIN = 'affiliate-product-highlights'; - const VERSION = '0.1.1'; + const VERSION = '0.2.0'; const REST_NAMESPACE = 'phft/v1'; @@ -57,6 +57,8 @@ public function __construct($file){ add_shortcode('product-highlights', [$this, 'display_products_shortcode']); + add_shortcode('phft-link', [$this, 'product_link_shortcode']); + add_action('init', function(){ add_rewrite_rule('^phft/([^/]+)/?$', 'index.php?phft_product=$matches[1]', 'top'); add_rewrite_tag('%phft_product%', '([^/]+)'); @@ -80,14 +82,14 @@ public static function activate(){ global $wpdb; - // Set the table name $products_table = $wpdb->prefix . 'phft_products'; - // Set the character set and collation for the table + $images_table = $wpdb->prefix.'phft_images'; + $charset_collate = $wpdb->get_charset_collate(); // Define the table schema - $products_sql = "CREATE TABLE $products_table ( + $sql = "CREATE TABLE $products_table ( id bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT, feed_id bigint(20) UNSIGNED NOT NULL, campaign_id bigint(20) UNSIGNED DEFAULT NULL, @@ -109,12 +111,7 @@ public static function activate(){ KEY product_name_idx (product_name) ) $charset_collate;"; - // Create or update the table using dbDelta - dbDelta($products_sql); - - $images_table = $wpdb->prefix.'phft_images'; - - $images_sql = "CREATE TABLE $images_table ( + $sql .= "CREATE TABLE $images_table ( id bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT, feed_id bigint(20) UNSIGNED NOT NULL, product_id bigint(20) UNSIGNED NOT NULL, @@ -122,10 +119,11 @@ public static function activate(){ wp_media_id bigint(20) UNSIGNED NOT NULL, imported_at datetime NOT NULL, PRIMARY KEY (id), + FOREIGN KEY (product_id) REFERENCES $products_table(id) ON DELETE CASCADE, KEY image_url_idx (image_url) ) $charset_collate;"; - dbDelta($images_sql); + dbDelta($sql); if(!wp_next_scheduled('phft_update_feeds')){ wp_schedule_event(time(), 'daily', 'phft_update_feeds'); @@ -138,6 +136,20 @@ public static function deactivate(){ } + public static function uninstall(){ + global $wpdb; + + //Delete all sideloaded media + $wp_media = $wpdb->get_results("SELECT wp_media_id FROM {$wpdb->prefix}phft_images WHERE wp_media_id > 0"); + if($wp_media){ + foreach($wp_media as $media){ + wp_delete_attachment($media->wp_media_id, true); + } + } + + $wpdb->query("DROP TABLE IF EXISTS {$wpdb->prefix}phft_images,{$wpdb->prefix}phft_products"); + } + public function is_loaded(): bool { return $this->loaded; } @@ -300,7 +312,7 @@ public function display_products_shortcode($atts){ public function draw_product($product){ $locale = get_locale(); $fmt = numfmt_create( $locale, NumberFormatter::CURRENCY ); - $product_url = esc_url(home_url('/phft/' . urlencode($product->slug))); + $product_url = esc_url(trailingslashit(home_url('/phft/' . urlencode($product->slug)))); $has_sale = $product->product_original_price > $product->product_price; @@ -320,6 +332,26 @@ public function draw_product($product){ } + public function product_link_shortcode($atts, $content){ + global $wpdb; + + $atts = shortcode_atts([ + 'product_id' => null, + ], $atts, 'phft-link'); + + if($atts['product_id'] === null){ + return $content; + } + + $product_id = $atts['product_id']; + + $product = $wpdb->get_row($wpdb->prepare("SELECT * FROM {$wpdb->prefix}phft_products WHERE id = %d", $product_id)); + + $product_url = esc_url(trailingslashit(home_url('/phft/' . urlencode($product->slug)))); + + return ''.$content.''; + } + public function save_feed_metabox($post_id, $post, $update){ if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) { return; @@ -346,8 +378,6 @@ public function delete_feed($post_id, \WP_Post $post){ global $wpdb; - $wpdb->query($wpdb->prepare("DELETE FROM {$wpdb->prefix}phft_products WHERE feed_id = %d", $post_id)); - $wp_media = $wpdb->get_results($wpdb->prepare("SELECT wp_media_id FROM {$wpdb->prefix}phft_images WHERE feed_id = %d AND wp_media_id > 0", $post_id)); if($wp_media){ @@ -356,8 +386,7 @@ public function delete_feed($post_id, \WP_Post $post){ } } - - $wpdb->query($wpdb->prepare("DELETE FROM {$wpdb->prefix}phft_images WHERE feed_id = %d", $post_id)); + $wpdb->query($wpdb->prepare("DELETE FROM {$wpdb->prefix}phft_products WHERE feed_id = %d", $post_id)); } diff --git a/src/php/Provider/Daisycon/ProductMapping.php b/src/php/Provider/Daisycon/ProductMapping.php new file mode 100644 index 0000000..862a5d5 --- /dev/null +++ b/src/php/Provider/Daisycon/ProductMapping.php @@ -0,0 +1,45 @@ +product_xml = $product_xml; + } + + /** + * @inheritDoc + */ + public function get_product_mapping(): array { + return [ + 'sku' => (string)$this->product_xml->sku, + 'product_name' => (string)$this->product_xml->title, + 'product_price' => number_format((float)$this->product_xml->price, 2,'.', ''), + 'product_original_price' => number_format((float)$this->product_xml->price_old, 2,'.', ''), + 'product_currency' => (string)$this->product_xml->currency, + 'product_url' => sanitize_url((string)$this->product_xml->link), + 'product_description' => (string)$this->product_xml->description, + 'product_ean' => (string)$this->product_xml->ean, + ]; + } + + /** + * @inheritDoc + */ + public function get_product_images(): array { + return array_map(function($image) { + return (string)$image->image->location; + }, iterator_to_array($this->product_xml->images)); + } +} diff --git a/src/php/Provider/ProductMapInterface.php b/src/php/Provider/ProductMapInterface.php index 498b5a8..7adacbe 100644 --- a/src/php/Provider/ProductMapInterface.php +++ b/src/php/Provider/ProductMapInterface.php @@ -15,7 +15,6 @@ public function __construct(SimpleXMLElement $product_xml); * Get normalized array mapping. * * @return array: - * - 'feed_id': int * - 'campaign_id': int|null * - 'product_id': int|null * - 'sku': string|null @@ -25,7 +24,6 @@ public function __construct(SimpleXMLElement $product_xml); * - 'product_url': string * - 'product_description': string * - 'product_ean': string|null - * - 'imported_at': string (datetime in 'Y-m-d H:i:s' format) */ public function get_product_mapping(): array;