Files
2026-04-28 15:13:50 +02:00

701 lines
23 KiB
PHP

<?php
/**
* @package WP Product Feed Manager/Data/Functions
* @version 2.7.0
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Converts a string containing a date-time stamp as stored in the metadata to a date time string
* that can be used in a feed file
*
* @param string $date_stamp The timestamp that needs to be converted to a string that can be stored in a feed file
*
* @return string A string containing the time or an empty string if the $date_stamp is empty
* @since 1.1.0
*
*/
function wppfm_convert_price_date_to_feed_format( $date_stamp ) {
if ( $date_stamp ) {
return gmdate( 'Y-m-d\TH:iO', $date_stamp );
} else {
return '';
}
}
/**
* After a channel has been updated, this function decreases the 'wppfm_channels_to_update' option with one
*
* @since 1.4.1
*/
function wppfm_decrease_update_ready_channels() {
$old = get_option( 'wppfm_channels_to_update' );
if ( $old > 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 );
}