0 ) { update_option( 'wppfm_channels_to_update', $old - 1 ); } else { update_option( 'wppfm_channels_to_update', 0 ); } } /** * Checks the current database version and updates it if required * * @since 2.4.0 */ function wppfm_check_db_version() { $db_management = new WPPFM_Database_Management(); $db_management->verify_db_version(); } /** * Checks if a specific source key is money related key or not * * @param string $key The source key to be checked * * @return boolean True if the source key is money-related, false if not * @since 1.1.0 * */ function wppfm_meta_key_is_money( $key ) { // money keys $special_price_keys = array( '_max_variation_price', '_max_variation_regular_price', '_max_variation_sale_price', '_min_variation_price', '_min_variation_regular_price', '_min_variation_sale_price', '_regular_price', '_regular_price_with_tax', '_regular_price_without_tax', '_sale_price', '_sale_price_with_tax', '_sale_price_without_tax', '_max_group_price', '_min_group_price', 'regular_price', 'sale_price', ); return in_array( $key, $special_price_keys, true ); } /** * Takes a value and formats it to a money value using the WooCommerce thousands separator, decimal separator and number of decimal values * * @param string $money_value The money values to be formatted * @param string $feed_language Selected Language in WPML add-on, leave empty if no exchange rate correction is required @since 1.9.0 * @param string $feed_currency Selected currency in WOOCS add-on, leave empty if no correction is required @since 2.28.0. * * @return string A formatted money value * @since 1.9.0 added WPML support * @since 2.28.0 Switched to the formal wc functions to get the separator and number of decimal values. * @since 2.28.0 Added support for the WooCommerce Currency Switcher plugin. * @since 2.31.0 Added the wppfm_feed_price_thousands_separator, wppfm_feed_price_decimal_separator and wppfm_feed_price_decimals filters. * @since 2.36.1 Return an empty string if the $money_value parameter is an empty string. This is required to allow filtering feed attributes on "is empty" parameters. * * @since 1.1.0 */ function wppfm_prep_money_values( $money_value, $feed_language = '', $feed_currency = '' ) { if ( '' === $money_value ) { return $money_value; } $thousand_separator = apply_filters( 'wppfm_feed_price_thousands_separator', wc_get_price_thousand_separator() ); if ( ! is_float( $money_value ) ) { $val = wppfm_number_format_parse( $money_value ); $money_value = floatval( $val ); } if ( has_filter( 'wppfm_woocs_exchange_money_values' ) ) { // WOOCS Support. $money_value = apply_filters( 'wppfm_woocs_exchange_money_values', $money_value, $feed_currency ); } if ( has_filter( 'wppfm_wpml_exchange_money_values' ) ) { // WPML Support. return apply_filters( 'wppfm_wpml_exchange_money_values', $money_value, $feed_language ); } else { $decimal_point = apply_filters( 'wppfm_feed_price_decimal_separator', wc_get_price_decimal_separator() ); $number_decimals = apply_filters( 'wppfm_feed_price_decimals', wc_get_price_decimals() ); // To prevent Google Merchant Centre to interpret a thousand separator as a decimal separator, we need to remove // the thousand separators if the decimals setting in WC is 0 and a period is used as a decimal separator. // E.g., 1.452 would be interpreted by Google as 1,452. // @since 2.11.0 if ( 0 === $number_decimals && '.' === $thousand_separator ) { $thousand_separator = ''; } return number_format( $money_value, $number_decimals, $decimal_point, $thousand_separator ); } } /** * Checks if there are invalid backups * * @return boolean true, if there are no backups or these backups are current * @since 1.8.0 * */ function wppfm_check_backup_status() { if ( ! WPPFM_Db_Management::invalid_backup_exist() ) { return true; } else { return false; } } /** * Checks a folder given by $path for SQL files and returns their names including the path * * @param $path * @since 2.6.0 * * @return array */ function wppfm_list_sql_files( $path ) { $files = array(); if ( is_dir( $path ) ) { $handle = opendir( $path ); if ( $handle ) { while ( false !== ( $name = readdir( $handle ) ) ) { // phpcs:ignore WordPress.CodeAnalysis.AssignmentInCondition.FoundInWhileCondition if ( preg_match( '/[a-zA-Z0-9-_ ]{2,}[.](sql)$/', $name ) ) { $files[] = $path . '/' . $name; } } } } return $files; } /** * Forces the database to load and update and adds the auto update cron event if it does not exist * * @since 1.9.0 * @return boolean */ function wppfm_reinitiate_plugin() { wppfm_check_feed_update_schedule(); wppfm_clear_feed_process_data(); // remakes the database $db = new WPPFM_Database_Management(); $db->force_reinitiate_db(); $plugin_prefixes = apply_filters( 'wppfm_edd_plugin_prefix_list', array( 'wppfm' ) ); // resets the license nr foreach ( $plugin_prefixes as $plugin_prefix ) { delete_option( $plugin_prefix . '_lic_status' ); delete_option( $plugin_prefix . '_lic_status_date' ); delete_option( $plugin_prefix . '_lic_key' ); delete_option( $plugin_prefix . '_lic_expires' ); delete_option( $plugin_prefix . '_license_notice_suppressed' ); } // reset the keyed options WPPFM_Db_Management::clean_options_table(); do_action( 'wppfm_plugin_reinitialized' ); return true; } /** * Returns the global WP_Filesystem object. * * @since 3.12.0 * @since 3.14.0 Added the wppfm_initialize_wp_filesystem function to initialize the WP_Filesystem object even if the. * @return object WP_Filesystem */ function wppfm_get_wp_filesystem() { global $wp_filesystem; if ( ! $wp_filesystem instanceof WP_Filesystem_Base ) { $initialized = wppfm_initialize_wp_filesystem(); if ( false === $initialized ) { die( esc_html__( 'WP_Filesystem could not be initialized', 'wp-product-feed-manager' ) ); } } if ( WP_Filesystem() ) { return $wp_filesystem; } else { $credentials = request_filesystem_credentials( '', 'ftp' ); if ( $credentials && WP_Filesystem( $credentials ) ) { return $wp_filesystem; } else { die( esc_html__( 'Unable to initialize WP_Filesystem!', 'wp-product-feed-manager' ) ); } } } /** * Initializes the WP_Filesystem object. * * @since 3.14.0 * @return bool True if the WP_Filesystem object was successfully initialized, false if not. */ function wppfm_initialize_wp_filesystem() { global $wp_filesystem; if ( $wp_filesystem instanceof WP_Filesystem_Base ) { return true; } require_once ABSPATH . 'wp-admin/includes/file.php'; $method = wppfm_get_wp_filesystem_method_or_direct(); $initialized = false; if ( 'direct' === $method ) { $initialized = WP_Filesystem(); } else { ob_start(); $credentials = request_filesystem_credentials( '' ); ob_end_clean(); $initialized = $credentials && WP_Filesystem( $credentials ); } return is_null( $initialized ) ? false : $initialized; } /** * Returns the method that the WP_Filesystem object should be initialized with. * * @since 3.14.0 * @return string The method that the WP_Filesystem object should be initialized with. */ function wppfm_get_wp_filesystem_method_or_direct() { $method = 'direct'; if ( defined( 'FS_METHOD' ) && 'direct' !== FS_METHOD ) { $method = FS_METHOD; } return $method; } /** * Adds a new line to the end of a file. * Uses native PHP file operations with append mode and file locking to prevent race conditions. * Falls back to WP_Filesystem for FTP/SSH scenarios. * * @param string $file_path The path to the file. * @param string $new_line The new line to be added. * @param bool $add_crt Add a carriage return at the end of the line. Default is false. * * @since 3.12.0 * @since 3.15.0 - Improved to use true append mode with file locking to prevent race conditions and corruption. * @return bool|int The number of bytes written, or false on failure. */ function wppfm_append_line_to_file( $file_path, $new_line, $add_crt = false ) { // Prepare the line to write $line_to_write = $new_line . ( $add_crt ? PHP_EOL : '' ); // Track file operation for performance monitoring do_action( 'wppfm_before_file_write', $line_to_write ); // Try to use native PHP file operations first (most efficient and safe for local files) // This prevents race conditions by using append mode with file locking $real_path = wppfm_get_real_file_path( $file_path ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_is_writable -- Prefer native checks for direct FS access before WP_Filesystem fallback. if ( $real_path && is_writable( dirname( $real_path ) ) ) { // Use native PHP file operations with append mode and locking // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen -- Native append is faster when direct FS access is available. $handle = @fopen( $real_path, 'a' ); if ( false !== $handle ) { // Try to acquire exclusive lock to prevent race conditions // LOCK_EX = exclusive lock (write lock), blocks other processes // First try non-blocking lock (fast path) $lock_acquired = @flock( $handle, LOCK_EX | LOCK_NB ); // If non-blocking lock failed, try blocking lock with retries // This ensures data integrity even under high contention if ( ! $lock_acquired ) { $max_retries = 3; $retry_delay = 100000; // 100ms in microseconds $retry_count = 0; while ( $retry_count < $max_retries && ! $lock_acquired ) { usleep( $retry_delay ); $lock_acquired = @flock( $handle, LOCK_EX | LOCK_NB ); $retry_count++; } // If still no lock after retries, try blocking lock (will wait) // This is safe because append operations are very fast if ( ! $lock_acquired ) { $lock_acquired = @flock( $handle, LOCK_EX ); } } if ( $lock_acquired ) { // Write the line // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fwrite -- Native write keeps append atomic before WP_Filesystem fallback. $bytes_written = @fwrite( $handle, $line_to_write ); // Release lock immediately after write @flock( $handle, LOCK_UN ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose -- Close native handle used for direct writes. @fclose( $handle ); if ( false !== $bytes_written ) { return $bytes_written; } } else { // Could not acquire lock after all attempts, close handle and fall through to fallback // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose -- Close native handle before WP_Filesystem fallback. @fclose( $handle ); } } } // Fallback to WP_Filesystem method (for FTP/SSH or if native operations fail) // This maintains compatibility but is less efficient for large files $wp_filesystem = wppfm_get_wp_filesystem(); // For fallback, we still need to read-all-write-all, but this should be rare $contents = $wp_filesystem->exists( $file_path ) ? $wp_filesystem->get_contents( $file_path ) : ''; $contents .= $line_to_write; return $wp_filesystem->put_contents( $file_path, $contents, FS_CHMOD_FILE ); } /** * Gets the real file system path for a file. * Handles both absolute and relative paths, and resolves WP_Filesystem paths when possible. * * @param string $file_path The file path (may be absolute or relative). * * @since 3.15.0 * @return string|false The real file system path, or false if cannot be determined. */ function wppfm_get_real_file_path( $file_path ) { if ( empty( $file_path ) ) { return false; } // Normalize the path (remove any trailing slashes, resolve . and ..) $file_path = rtrim( $file_path, '/' ); // If file exists, get its realpath if ( file_exists( $file_path ) && is_file( $file_path ) ) { $real_path = realpath( $file_path ); if ( $real_path ) { return $real_path; } } // If file doesn't exist yet (new file), try to resolve the directory $dir_path = dirname( $file_path ); $file_name = basename( $file_path ); // Check if directory exists and is writable // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_is_writable -- Direct path writable checks avoid unnecessary filesystem init. if ( file_exists( $dir_path ) && is_dir( $dir_path ) && is_writable( $dir_path ) ) { $real_dir = realpath( $dir_path ); if ( $real_dir ) { return $real_dir . '/' . $file_name; } } // Try to resolve using WPPFM_FEEDS_DIR if defined if ( defined( 'WPPFM_FEEDS_DIR' ) ) { // Check if the path contains WPPFM_FEEDS_DIR if ( strpos( $file_path, WPPFM_FEEDS_DIR ) === 0 ) { // Path already contains WPPFM_FEEDS_DIR, try to resolve it if ( file_exists( WPPFM_FEEDS_DIR ) && is_dir( WPPFM_FEEDS_DIR ) ) { $real_feeds_dir = realpath( WPPFM_FEEDS_DIR ); if ( $real_feeds_dir ) { // Extract relative path from WPPFM_FEEDS_DIR $relative_path = substr( $file_path, strlen( WPPFM_FEEDS_DIR ) ); $relative_path = ltrim( $relative_path, '/' ); return $real_feeds_dir . '/' . $relative_path; } } } // Try to find file in WPPFM_FEEDS_DIR by filename only $feeds_dir_path = WPPFM_FEEDS_DIR . '/' . $file_name; if ( file_exists( $feeds_dir_path ) && is_file( $feeds_dir_path ) ) { $real_path = realpath( $feeds_dir_path ); if ( $real_path ) { return $real_path; } } // If directory exists, construct path for new file // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_is_writable -- Direct dir checks preferred before WP_Filesystem fallback. if ( file_exists( WPPFM_FEEDS_DIR ) && is_dir( WPPFM_FEEDS_DIR ) && is_writable( WPPFM_FEEDS_DIR ) ) { $real_feeds_dir = realpath( WPPFM_FEEDS_DIR ); if ( $real_feeds_dir ) { return $real_feeds_dir . '/' . $file_name; } } } // If we can't determine the real path, return false to trigger fallback return false; } /** * Checks if the feed update schedule is registered. If it's missing, it will reactivate it again. * * @since 2.20.0 */ function wppfm_check_feed_update_schedule() { $hook = 'wppfm_feed_update_schedule'; $desired_schedule = 'wppfm_feed_update_interval'; // Prefer the centralized scheduler when available (keeps upgrades consistent). if ( function_exists( 'wppfm_schedule_feed_update_event' ) ) { wppfm_schedule_feed_update_event(); return; } $current_schedule = wp_get_schedule( $hook ); if ( $desired_schedule !== $current_schedule ) { // Normalize older installs that were scheduled hourly. wp_clear_scheduled_hook( $hook ); wp_schedule_event( time() + MINUTE_IN_SECONDS, $desired_schedule, $hook ); } } /** * Recursively implodes an array * * @since 2.8.0 * * @param array|string $array * @param string $glue * @param bool $include_keys * @param bool $trim_all * * @return string */ function wppfm_recursive_implode( $array, $glue = ',', $include_keys = false, $trim_all = true ) { $glued_string = ''; // @since 2.41.0 if ( ! is_array( $array ) ) { $array = json_decode( $array, true ); } // @since 2.41.0 if ( ! $array ) { return ''; } // Recursively iterates array and adds key/value to glued string array_walk_recursive( $array, function ( $value, $key ) use ( $glue, $include_keys, &$glued_string ) { $include_keys and $glued_string .= $key . ' => '; $glued_string .= $value . $glue; } ); // Removes last $glue from string if ( strlen( $glue ) > 0 && $glued_string ) { $glued_string = substr( $glued_string, 0, - strlen( $glue ) ); } // Trim ALL whitespace if ( $trim_all && $glued_string ) { /** @noinspection PhpRegExpRedundantModifierInspection */ $glued_string = preg_replace( '/(\s)/ixsm', '', $glued_string ); } return $glued_string; } function wppfm_clear_feed_process_data() { WPPFM_Feed_Controller::clear_feed_queue(); WPPFM_Feed_Controller::set_feed_processing_flag(); WPPFM_Db_Management::clean_options_table(); WPPFM_Db_Management::reset_status_of_failed_feeds(); WPPFM_Db_Management::reset_feed_runtime_state(); do_action( 'wppfm_feed_process_data_cleared' ); return true; } /** * Takes a string with spaces and capital letters and converts it to a string with dashes and lower case letters * * @param $original_string * * @return string */ function wppfm_convert_string_with_spaces_to_lower_case_string_with_dashes( $original_string ) { return strtolower( str_replace( ' ', '-', $original_string ) ); } function wppfm_convert_string_with_dashes_to_upper_case_string_with_spaces( $original_string ) { return null !== $original_string ? ucwords( str_replace( '-', ' ', $original_string ) ) : ''; } /** * Converts any number string to a string with a number that has no thousand separators * and a period as a decimal separator * * @param string $number_string * * @since 2.28.0 Switched to the formal wc functions to get the separator and number of decimal values. * * @return string */ function wppfm_number_format_parse( $number_string ) { $decimal_separator = wc_get_price_decimal_separator(); $thousand_separator = wc_get_price_thousand_separator(); // convert a number string that is an actual standard number format whilst the woocommerce options are not standard // to the woocommerce standard. // This sometimes happens with meta-values if ( ! empty( $decimal_separator ) && strpos( $number_string, $decimal_separator ) === false ) { $number_string = ! empty( $thousand_separator ) && strpos( $number_string, $thousand_separator ) === false ? $number_string : str_replace( $thousand_separator, $decimal_separator, $number_string ); } $no_thousands_sep = str_replace( $thousand_separator, '', $number_string ); return '.' !== $decimal_separator ? str_replace( $decimal_separator, '.', $no_thousands_sep ) : $no_thousands_sep; } /** * returns the path to the feed file including feed name and extension * * @param string $feed_name * * @return string */ function wppfm_get_file_path( $feed_name ) { $forbidden_name_chars = wppfm_forbidden_file_name_characters(); $feed_name = str_replace( $forbidden_name_chars, '-', $feed_name ); // previous to plugin version 1.3.0 feeds where stored in the plugins, but after that version they are stored in the upload folder // @since 3.17.0 - Use dirname() to get the plugins directory from the plugin directory. $legacy_plugin_path = dirname( WPPFM_PLUGIN_DIR ) . '/wp-product-feed-manager-support/feeds/' . $feed_name; if ( file_exists( $legacy_plugin_path ) ) { return $legacy_plugin_path; } elseif ( file_exists( WPPFM_FEEDS_DIR . '/' . $feed_name ) ) { return WPPFM_FEEDS_DIR . '/' . $feed_name; } else { // as of version 1.5.0, all spaces in new filenames are replaced by a dash return WPPFM_FEEDS_DIR . '/' . $feed_name; } } /** * Returns the url of the feed file including feed name and extension. * * @param string $feed_name Name of the feed file. * * @return string URL to the feed file. */ function wppfm_get_file_url( $feed_name ) { $forbidden_name_chars = wppfm_forbidden_file_name_characters(); $feed_name = str_replace( $forbidden_name_chars, '-', $feed_name ); // previous to plugin version 1.3.0 feeds where stored in the plugins, but after that version they are stored in the upload folder // @since 3.17.0 - Use dirname() to get the plugins directory from the plugin directory. $legacy_plugin_path = dirname( WPPFM_PLUGIN_DIR ) . '/wp-product-feed-manager-support/feeds/' . $feed_name; if ( file_exists( $legacy_plugin_path ) ) { // Use plugins_url() with the legacy plugin file path for proper URL resolution. $legacy_plugin_file = dirname( WPPFM_PLUGIN_FILE ) . '/../wp-product-feed-manager-support/wp-product-feed-manager-support.php'; $file_url = plugins_url( 'feeds/' . $feed_name, $legacy_plugin_file ); } else { // as of version 1.5.0, all spaces in new filenames are replaced by a dash $file_url = WPPFM_UPLOADS_URL . '/wppfm-feeds/' . $feed_name; } return apply_filters( 'wppfm_feed_url', $file_url, $feed_name ); } /** * @return array with forbidden characters */ function wppfm_forbidden_file_name_characters() { return array( ' ', '<', '>', ':', '?', ',', "'", '{', '}', '#' ); // characters that are not allowed in a feed file name } /** * For backward compatibility, the old feed statuses are converted to all lowercases and without spaces * * @param array $list * * @since 2.1.0 * * @noinspection PhpParameterByRefIsNotUsedAsReferenceInspection */ function wppfm_correct_old_feeds_list_status( &$list ) { for ( $i = 0; $i < count( $list ); $i ++ ) { $list[ $i ]->status = strtolower( str_replace( ' ', '_', $list[ $i ]->status ) ); } } /** * Checks if the WooCommerce plugin is installed and active * * @return boolean true if WooCommerce is installed and active, false if not * @since 2.3.0 */ function wppfm_wc_installed_and_active() { return is_plugin_active( 'woocommerce/woocommerce.php' ) || is_plugin_active_for_network( 'woocommerce/woocommerce.php' ); } /** * Checks if the WooCommerce plugin has the minimal required version * * @return boolean true if WooCommerce version is at least 3.0.0 * @since 2.3.0 * @since 3.16.0 - Changed the use of WPPFM_PLUGIN_DIR + '..' to find the plugins folder, to the use of WP_PLUGIN_DIR. */ function wppfm_wc_min_version_required() { // To prevent several PHP Warnings if the WC folder name has been changed whilst the plugin is still registered. // @since 2.11.0. // Use dirname() to get the plugins directory from the plugin directory. $wc_plugin_file = dirname( WPPFM_PLUGIN_DIR ) . '/woocommerce/woocommerce.php'; if ( ! file_exists( $wc_plugin_file ) ) { return false; } $wc_version = get_plugin_data( $wc_plugin_file )['Version']; return version_compare( $wc_version, WPPFM_MIN_REQUIRED_WC_VERSION, '>=' ); } /** * Stores the latest blog data in the WordPress options, maintaining a maximum of four entries. * * @param array $blog_data An associative array containing the weblog data to store. * * @return void * @since 3.14.0. */ function wppfm_store_latest_blog( $blog_data ) { $current_weblogs = get_option( 'wppfm_latest_weblogs', array() ); // Add the new weblog data to the beginning of the array. array_unshift( $current_weblogs, $blog_data); // Keep only the four latest weblogs. $current_weblogs = array_slice( $current_weblogs, 0, 4 ); // Update the option with the new array. update_option( 'wppfm_latest_weblogs', $current_weblogs ); }