<?php
/**
 * Plugin Name: Product Feed Manager for WooCommerce
 * Plugin URI: https://www.wpmarketingrobot.com
 * Description: An easy-to-use WordPress plugin that generates and submits your product feeds to merchant centres.
 * Author: WP Marketing Robot
 * Author URI: https://www.wpmarketingrobot.com
 * Developer: Michel Jongbloed
 * Developer URI: https://www.wpmarketingrobot.com
 * Version: 2.23.6
 * Modified: 18-05-2026
 * WC requires at least: 8.4
 * WC tested up to: 10.7.0
 * License: GPL-3.0-or-later
 * License URI: https://www.gnu.org/licenses/gpl-3.0.html
 *
 * This plugin is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * You can read the GNU General Public License here <http://www.gnu.org/licenses/>.
 * Requires at least: 6.5
 * Requires Plugins: woocommerce
 * Tested up to: 6.9
 *
 * @package WordPress
 *
 * Text Domain: wp-product-feed-manager
 * Domain Path: /languages
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

if ( ! class_exists( 'WP_Product_Feed_Manager' ) ) :

	/**
	 * The Main WP_Product_Feed_Manager Class.
	 *
	 * @class WP_Product_Feed_Manager.
	 */
	final class WP_Product_Feed_Manager {

		/**
		 * Version number.
		 *
		 * @var string  Containing the version number of the plugin.
		 */
		public $version = '2.23.6';

		/**
		 * Author Name.
		 *
		 * @var string  Containing the author name.
		 */
		public $author = 'Michel Jongbloed';

		/**
		 * Force single instance.
		 *
		 * @var WP_Product_Feed_Manager Single instance.
		 */
		private static $instance = null;

		/**
		 * Returns the Singleton instance of this class.
		 *
		 * @static
		 * @access public
		 * @return  WP_Product_Feed_Manager Main instance.
		 */
		public static function get_instance() {
			if ( is_null( self::$instance ) ) {
				self::$instance = new self();
			}

			return self::$instance;
		}

		/**
		 * Cloning is not allowed.
		 *
		 * @since 0.9.1
		 */
		public function __clone() {
			_doing_it_wrong( __FUNCTION__, esc_html__( 'Cloning is not allowed', 'wp-product-feed-manager' ), '0.9.1' );
		}

		/**
		 * Unserializing instances of this class are not allowed.
		 *
		 * @since 0.9.1
		 */
		public function __wakeup() {
			_doing_it_wrong(
				__FUNCTION__,
				esc_html__( 'Unserializing instances of this class is not allowed', 'wp-product-feed-manager' ),
				'0.9.1'
			);
		}

		/**
		 * WP_Product_Feed_Manager Constructor.
		 *
		 * @since 0.9.1
		 */
		private function __construct() {
			// Set the constants to be used in this plugin.
			$this->define_constants();

			// Hooks.
			$this->hooks();

			// Includes.
			$this->includes();


			// Register my schedule.
			add_action( 'wppfm_feed_update_schedule', array( $this, 'activate_feed_update_schedules' ) );
			add_action( 'wp_ajax_wppfm_dismiss_admin_notice', array( $this, 'wppfm_dismiss_admin_notice' ) );

			// Set up localisation.
			// WordPress loads plugin translations just-in-time from Text Domain/Domain Path metadata;
			// keep only the mofile override so bundled translations are preferred when available.
			// @since 3.18.0 Force premium builds to use bundled translations.
			add_filter( 'load_textdomain_mofile', array( $this, 'prefer_bundled_translations' ), 10, 2 );

			// @since 3.23.0 Prevent stale translation cache paths from causing transient include warnings.
			add_filter( 'pre_get_language_files_from_path', array( $this, 'sanitize_cached_language_file_paths' ), 10, 2 );

			// Declare compatibility with custom order tables.
			add_action( 'before_woocommerce_init', array( $this, 'declare_compatibility_for_custom_order_tables' ) );

			// Invalidate custom product fields cache when third-party keywords option changes.
			add_action( 'update_option_wppfm_third_party_attribute_keywords', array( $this, 'invalidate_custom_product_fields_cache' ), 10, 3 );

			do_action( 'wppfm_product_feed_manager_loaded' );
			// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Legacy hook retained for backward compatibility with existing third-party integrations.
			do_action( 'wp_product_feed_manager_loaded' );
		}

		/**
		 * Defines a few important constants.
		 */
		private function define_constants() {
			// Store the main plugin file path.
			if ( ! defined( 'WPPFM_PLUGIN_FILE' ) ) {
				define( 'WPPFM_PLUGIN_FILE', __FILE__ );
			}

			// Store the name of the plugin.
			if ( ! defined( 'WPPFM_PLUGIN_NAME' ) ) {
				define( 'WPPFM_PLUGIN_NAME', 'wp-product-feed-manager' );
			}

			// Store the directory of the plugin.
			if ( ! defined( 'WPPFM_PLUGIN_DIR' ) ) {
				define( 'WPPFM_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
			}

			// Store the plugin constructor.
			if ( ! defined( 'WPPFM_PLUGIN_CONSTRUCTOR' ) ) {
				define( 'WPPFM_PLUGIN_CONSTRUCTOR', plugin_basename( __FILE__ ) );
			}

			// Store the url of the plugin.
			if ( ! defined( 'WPPFM_PLUGIN_URL' ) ) {
				define( 'WPPFM_PLUGIN_URL', plugin_dir_url( __FILE__ ) );
			}

			// Store the version of my plugin.
			if ( ! defined( 'WPPFM_VERSION_NUM' ) ) {
				define( 'WPPFM_VERSION_NUM', $this->version );
			}

			// Store the version id of my plugin.
			// @since 2.38.0.
			if ( ! defined( 'WPPFM_PLUGIN_VERSION_ID' ) ) {
				define( 'WPPFM_PLUGIN_VERSION_ID', 'free' );
			}

			// Store the plugin distributor.
			if ( ! defined( 'WPPFM_PLUGIN_DISTRIBUTOR' ) ) {
				define( 'WPPFM_PLUGIN_DISTRIBUTOR', 'wpmarketingrobot' );
			}

			// Store the transient alive time.
			if ( ! defined( 'WPPFM_TRANSIENT_LIVE' ) ) {
				define( 'WPPFM_TRANSIENT_LIVE', 20 * MINUTE_IN_SECONDS );
			}

			// Store the time before a feed gets a failed label.
			if ( ! defined( 'WPPFM_DELAY_FAILED_LABEL' ) ) {
				define( 'WPPFM_DELAY_FAILED_LABEL', MINUTE_IN_SECONDS );
			}

			// Store the url to wpmarketingrobot.com.
			if ( ! defined( 'WPPFM_EDD_SL_STORE_URL' ) ) {
				define( 'WPPFM_EDD_SL_STORE_URL', 'https://www.wpmarketingrobot.com/' );
			}

			// Store the link to the support page.
			if ( ! defined( 'WPPFM_SUPPORT_PAGE_URL' ) ) {
				define( 'WPPFM_SUPPORT_PAGE_URL', 'www.wpmarketingrobot.com/support/' );
			}

			// Store the plugin title.
			if ( ! defined( 'WPPFM_EDD_SL_ITEM_NAME' ) ) {
				define( 'WPPFM_EDD_SL_ITEM_NAME', 'Product Feed Manager for WooCommerce' );
			}

			// Store the plugin title.
			if ( ! defined( 'WPPFM_MIN_REQUIRED_WC_VERSION' ) ) {
				define( 'WPPFM_MIN_REQUIRED_WC_VERSION', '3.0.0' );
			}

			// Store the base uploads' folder, should also work in a multi-site environment.
			if ( ! defined( 'WPPFM_UPLOADS_DIR' ) ) {
				$wp_upload_dir = wp_get_upload_dir(); // @since 2.10.0 switched from wp_upload_dir to wp_get_upload_dir.
				$upload_dir    = is_multisite() && defined( 'UPLOADS' ) ? UPLOADS : $wp_upload_dir['basedir'];

				if ( ! file_exists( $upload_dir ) && ! is_dir( $upload_dir ) ) {
					define( 'WPPFM_UPLOADS_DIR', $wp_upload_dir['basedir'] );
				} else {
					define( 'WPPFM_UPLOADS_DIR', $upload_dir );
				}
			}

			if ( ! defined( 'WPPFM_UPLOADS_URL' ) ) {
				$wp_upload_dir = wp_upload_dir();

				// Correct baseurl for https if required.
				if ( is_ssl() ) {
					$url = str_replace( 'http://', 'https://', $wp_upload_dir['baseurl'] );
				} else {
					$url = $wp_upload_dir['baseurl'];
				}

				// @since 2.11.1 Added the wppfm_corrected_uploads_url filter.
				define( 'WPPFM_UPLOADS_URL', apply_filters( 'wppfm_corrected_uploads_url', $url ) );
			}

			// Channel packages (unzipped definitions, taxonomies). Multi-channel builds use
			// WPPFM_UPLOADS_DIR . '/wppfm-channels'; the free/Google-only build sets this to the bundled path below.
			if ( ! defined( 'WPPFM_CHANNEL_DATA_DIR' ) ) {
				define( 'WPPFM_CHANNEL_DATA_DIR', WPPFM_PLUGIN_DIR . 'includes/application' );
			}

			// Store the folder that contains the backup files.
			if ( ! defined( 'WPPFM_BACKUP_DIR' ) ) {
				define( 'WPPFM_BACKUP_DIR', WPPFM_UPLOADS_DIR . '/wppfm-backups' );
			}

			// Store the folder that contains the revision files.
			if ( ! defined( 'WPPFM_REVISIONS_DIR' ) ) {
				define( 'WPPFM_REVISIONS_DIR', WPPFM_UPLOADS_DIR . '/wppfm-revisions' );
			}

			// Store the folder that contains the feeds.
			if ( ! defined( 'WPPFM_FEEDS_DIR' ) ) {
				define( 'WPPFM_FEEDS_DIR', WPPFM_UPLOADS_DIR . '/wppfm-feeds' );
			}

			// Debug / HTTP / general plugin logs (uploads-backed; not inside the plugin package directory).
			if ( ! defined( 'WPPFM_LOGGINGS_DIR' ) ) {
				define( 'WPPFM_LOGGINGS_DIR', WPPFM_UPLOADS_DIR . '/wppfm-logs' );
			}
		}

		/**
		 * Sets the activation and deactivation hooks.
		 */
		private function hooks() {
			// Register's the activation, deactivation and uninstall hooks.
			register_activation_hook( __FILE__, array( &$this, 'on_activation' ) );
			register_deactivation_hook( __FILE__, array( &$this, 'on_deactivation' ) );
			register_shutdown_function( array( $this, 'log_errors' ) );
		}

		/**
		 * Includes the required files.
		 *
		 * @noinspection PhpIncludeInspection*/
		private function includes() {
			// Include the WordPress pluggable.php file on forehand to prevent a "Call to undefined function wp_get_current_user()" error.
			// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
			if ( is_admin() && isset( $_SERVER['PHP_SELF'] ) && basename( wp_unslash( $_SERVER['PHP_SELF'] ) ) === 'options-general.php' && 'email_template' === filter_input( INPUT_GET, 'page', FILTER_SANITIZE_FULL_SPECIAL_CHARS) ) {
				require_once ABSPATH . 'wp-includes/pluggable.php';
			}

			// To prevent a fatal error about not finding the is_plugin_active function.
			require_once ABSPATH . 'wp-admin/includes/plugin.php';

			// Include the admin menu and the includes file.
			require_once __DIR__ . '/includes/application/wppfm-feed-processing-support.php';
			require_once __DIR__ . '/includes/application/wppfm-feed-processor-functions.php';
			require_once __DIR__ . '/includes/application/wppfm-cron-functions.php';
			require_once __DIR__ . '/includes/user-interface/wppfm-admin-menu-functions.php';
			require_once __DIR__ . '/includes/user-interface/wppfm-admin-actions.php';
			require_once __DIR__ . '/includes/user-interface/wppfm-edit-feed-form-functions.php';
			require_once __DIR__ . '/includes/user-interface/wppfm-admin-filters.php';
			require_once __DIR__ . '/includes/data/wppfm-admin-functions.php';
			require_once __DIR__ . '/includes/data/wppfm-data-storage-functions.php';
			require_once __DIR__ . '/includes/user-interface/wppfm-messaging-functions.php';
			require_once __DIR__ . '/includes/user-interface/wppfm-url-functions.php';
			require_once __DIR__ . '/includes/user-interface/wppfm-woocommerce-actions.php';
			require_once __DIR__ . '/includes/wppfm-wpincludes.php';
			require_once __DIR__ . '/includes/packages/logger/wp-product-feed-manager-logger.php';


			if ( 'true' === get_option( 'wppfm_show_product_identifiers', 'false' ) ) {
			require_once __DIR__ . '/includes/user-interface/wppfm-product-identifiers.php'; // @since 2.10.0
			}

			// Include all required classes.
			wppfm_include_classes();
			wppfm_include_channels();


			// Include the integrated Product Review Feed Manager package.
			// @since 2.37.0
			require_once __DIR__ . '/includes/packages/review-feed-manager/wp-product-review-feed-manager.php';

			// Include the integrated Google Merchant Promotions Manager package.
			// @since 3.0.0
			require_once __DIR__ . '/includes/packages/promotions-feed-manager/wp-merchant-promotions-feed-manager.php';
		}


		/**
		 * Activate the feed update schedules.
		 */
		public function activate_feed_update_schedules() {
			require_once __DIR__ . '/includes/application/wppfm-cron-functions.php';
			wppfm_update_feeds();
		}

		/**
		 * Registers a dismiss notice action.
		 *
		 * @since 1.9.8
		 */
		public function wppfm_dismiss_admin_notice() {
			check_ajax_referer( 'wppfm-dismiss-admin-notice', 'nonce' );

			if ( ! current_user_can( 'manage_options' ) ) {
				wp_die( esc_html__( 'You are not allowed to do this.', 'wp-product-feed-manager' ), '', 403 );
			}

			if ( is_admin() ) {
				update_option( 'wppfm_license_notice_suppressed', true );
			}
		}

		/**
		 * Invalidates the custom product fields transient when the third-party
		 * attribute keywords option is updated.
		 *
		 * @param mixed  $old_value Old option value.
		 * @param mixed  $value     New option value.
		 * @param string $option    Option name.
		 */
		public function invalidate_custom_product_fields_cache( $old_value, $value, $option ) {
			delete_transient( 'wppfm_custom_product_fields_' . md5( (string) $old_value ) );
			delete_transient( 'wppfm_custom_product_fields_' . md5( (string) $value ) );
		}

		/**
		 * Registers a dismiss notice action, declaring compatibility with the WooCommerce custom order tables feature.
		 *
		 * @since 2.36.0
		 * @noinspection PhpFullyQualifiedNameUsageInspection
		 */
		public function declare_compatibility_for_custom_order_tables() {
			if ( class_exists( \Automattic\WooCommerce\Utilities\FeaturesUtil::class ) ) {
				\Automattic\WooCommerce\Utilities\FeaturesUtil::declare_compatibility( 'custom_order_tables', __FILE__ );
			}
		}

		/**
		 * Performs the required actions on activation of the plugin.
		 */
		public function on_activation() {
			// Add the required tables to the database.
			$wppfm_database = new WPPFM_Database_Management();
			$wppfm_database->make();

			// Ensure the automatic feed update cron exists and uses the expected interval.
			if ( function_exists( 'wppfm_schedule_feed_update_event' ) ) {
				wppfm_schedule_feed_update_event();
			} else {
				wp_schedule_event( time() + MINUTE_IN_SECONDS, 'wppfm_feed_update_interval', 'wppfm_feed_update_schedule' );
			}

			// @since 3.18.0 also prime the feed watchdog cron to recover orphaned queues.
			if ( apply_filters( 'wppfm_enable_feed_watchdog', true ) && ! wp_next_scheduled( 'wppfm_feed_watchdog_cron' ) ) {
				wp_schedule_event( time(), 'wppfm_feed_watchdog_interval', 'wppfm_feed_watchdog_cron' );
			}
		}

		/**
		 * Forces the plugin to load bundled translation files when available.
		 *
		 * @since 3.19.0
		 *
		 * @param string $mofile Loaded MO file path supplied by WordPress.
		 * @param string $domain Text domain that is being initialised.
		 *
		 * @return string
		 */
		public function prefer_bundled_translations( $mofile, $domain ) {
			if ( 'wp-product-feed-manager' !== $domain ) {
				return $mofile;
			}

			$locale = function_exists( 'determine_locale' ) ? determine_locale() : get_locale();
			$bundled_mo = WPPFM_PLUGIN_DIR . 'languages/wp-product-feed-manager-' . $locale . '.mo';

			if ( file_exists( $bundled_mo ) && is_readable( $bundled_mo ) ) {
				return $bundled_mo;
			}

			return $mofile;
		}

		/**
		 * Removes stale translation file paths from the WordPress translation cache.
		 *
		 * WordPress caches language file lists for up to one hour. During translation package updates,
		 * a cached `.l10n.php` path can briefly point to a file that no longer exists. This callback
		 * sanitizes the cached list for the plugins language directory before WordPress reads it.
		 *
		 * @since 3.23.0.3
		 *
		 * @param array|null $files Cached language files from earlier filters, or null.
		 * @param string     $path  Directory path currently being scanned for translation files.
		 *
		 * @return array|null
		 */
		public function sanitize_cached_language_file_paths( $files, $path ) {
			// Respect earlier filters that intentionally provide a custom file list.
			if ( null !== $files ) {
				return $files;
			}

			$plugins_language_path = trailingslashit( WP_LANG_DIR ) . 'plugins/';
			if ( $plugins_language_path !== $path ) {
				return null;
			}

			$cache_key    = md5( $path );
			$cached_files = wp_cache_get( $cache_key, 'translation_files' );

			if ( false === $cached_files || ! is_array( $cached_files ) ) {
				return null;
			}

			$valid_cached_files = array_values(
				array_filter(
					$cached_files,
					static function( $file_path ) {
						return is_string( $file_path ) && '' !== $file_path && file_exists( $file_path ) && is_readable( $file_path );
					}
				)
			);

			// Keep cache in sync so subsequent requests avoid the same stale-file warning.
			if ( count( $valid_cached_files ) !== count( $cached_files ) ) {
				wp_cache_set( $cache_key, $valid_cached_files, 'translation_files', HOUR_IN_SECONDS );
			}

			return $valid_cached_files;
		}

		/**
		 * Performs the required actions on deactivation of the plugin.
		 */
		public function on_deactivation() {
			// Stop the scheduled feed update actions.
			wp_clear_scheduled_hook( 'wppfm_feed_update_schedule' );
			// @since 3.18.0 unschedule the watchdog cron that keeps the queue healthy.
			wp_clear_scheduled_hook( 'wppfm_feed_watchdog_cron' );
			// Remove all keyed option items from the option table and clears any stuck feed processing data.
			wppfm_clear_feed_process_data();
		}

		/**
		 * Gets triggered when the plugin quits and is used to fetch fatal errors.
		 *
		 * @since 1.10.0
		 */
		public function log_errors() {
			$error = error_get_last();

			if ( $error ) {
				// Load the messaging code if not already done yet.
				require_once __DIR__ . '/includes/user-interface/wppfm-messaging-functions.php';

				// Fetch fatal errors.
				if ( E_ERROR === $error['type'] ) {
					// Load the required classes if not already done yet.
					if ( ! class_exists( 'WPPFM_Feed_Controller' ) ) {
						require_once __DIR__ . '/includes/application/class-wppfm-feed-controller.php';
					}
					if ( ! class_exists( 'WPPFM_Db_Management' ) ) {
						require_once __DIR__ . '/includes/data/class-wppfm-db-management.php';
					}

					// Clear the feed queue.
					WPPFM_Feed_Controller::clear_feed_queue();

					// The background process clearly has stopped.
					WPPFM_Feed_Controller::set_feed_processing_flag();

					// Remove all keyed option items from the option table.
					WPPFM_Db_Management::clean_options_table();

					wppfm_write_log_file(
						sprintf(
							'PHP Fatal error: %s in file %s on line %s',
							$error['message'],
							$error['file'],
							$error['line']
						)
					);
				} elseif ( E_WARNING === $error['type'] ) {
					if ( ( defined( 'WP_DEBUG' ) && WP_DEBUG ) ) {
						wppfm_write_log_file(
							sprintf(
								'PHP Warning: %s in file %s on line %s',
								$error['message'],
								$error['file'],
								$error['line']
							)
						);
					}
				}
			}
		}

	}

	// End of WP_Product_Feed_Manager class.

endif;

WP_Product_Feed_Manager::get_instance();
