woocommerce-smart-coupons

This commit is contained in:
Tony Volpe
2024-06-17 16:29:48 -04:00
parent c52ceefa4b
commit 39e0717541
237 changed files with 79624 additions and 0 deletions

View File

@@ -0,0 +1,156 @@
<?php
/**
* Functions to register client-side assets (scripts and stylesheets) for the
* Gutenberg block.
*
* @author StoreApps
* @since 4.0.0
* @version 1.0
*
* @package woocommerce-smart-coupons/includes/blocks
*/
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
if ( ! class_exists( 'WC_SC_Gutenberg_Coupon_Block' ) ) {
/**
* Class for handling Smart Coupons Shortcode
*/
class WC_SC_Gutenberg_Coupon_Block {
/**
* Variable to hold instance of WC_SC_Gutenberg_Coupon_Block
*
* @var $instance
*/
private static $instance = null;
/**
* Constructor
*/
private function __construct() {
global $wp_version;
if ( ! function_exists( 'is_plugin_active' ) ) {
require_once ABSPATH . 'wp-admin/includes/plugin.php';
}
if ( is_plugin_active( 'gutenberg/gutenberg.php' ) || version_compare( $wp_version, '5.0', '>=' ) ) {
add_action( 'init', array( $this, 'gutenberg_coupon_block_init' ) );
add_action( 'admin_enqueue_scripts', array( $this, 'gutenberg_coupon_add_inline_css' ) );
}
}
/**
* Get single instance of WC_SC_Gutenberg_Coupon_Block
*
* @return WC_SC_Gutenberg_Coupon_Block Singleton object of WC_SC_Gutenberg_Coupon_Block
*/
public static function get_instance() {
// Check if instance is already exists.
if ( is_null( self::$instance ) ) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Handle call to functions which is not available in this class
*
* @param string $function_name The function name.
* @param array $arguments Array of arguments passed while calling $function_name.
* @return result of function call
*/
public function __call( $function_name, $arguments = array() ) {
global $woocommerce_smart_coupon;
if ( ! is_callable( array( $woocommerce_smart_coupon, $function_name ) ) ) {
return;
}
if ( ! empty( $arguments ) ) {
return call_user_func_array( array( $woocommerce_smart_coupon, $function_name ), $arguments );
} else {
return call_user_func( array( $woocommerce_smart_coupon, $function_name ) );
}
}
/**
* Registers all block assets so that they can be enqueued through Gutenberg in
* the corresponding context.
*
* @see https://wordpress.org/gutenberg/handbook/blocks/writing-your-first-block-type/#enqueuing-block-scripts
*/
public function gutenberg_coupon_block_init() {
// Skip block registration if Gutenberg is not enabled/merged.
if ( ! function_exists( 'register_block_type' ) ) {
return;
}
$dir = dirname( __FILE__ );
$index_js = 'sc-gutenberg-block.js';
wp_register_script(
'coupon-block-editor-js',
plugins_url( $index_js, __FILE__ ),
array(
'wp-blocks',
'wp-i18n',
'wp-element',
'wp-components',
),
filemtime( "$dir/$index_js" ),
true
);
if ( shortcode_exists( 'smart_coupons' ) ) {
register_block_type(
'woocommerce-smart-coupons/coupon',
array(
'editor_script' => 'coupon-block-editor-js',
'attributes' => array(
'coupon_code' => array( 'type' => 'string' ),
),
'render_callback' => array( WC_SC_Shortcode::get_instance(), 'execute_smart_coupons_shortcode' ),
)
);
}
}
/**
* Our componenet is referring to smart-coupon.css as main css file.
* We cannot add register/enqueue a new CSS file to render coupon design in Gutenberg.
* And hence we are adding necessary files via wp_add_inline_style.
*/
public function gutenberg_coupon_add_inline_css() {
if ( ! wp_style_is( 'smart-coupon' ) ) {
wp_enqueue_style( 'smart-coupon' );
}
$coupon_style_attributes = WC_Smart_Coupons::get_instance()->get_coupon_style_attributes();
$gutenberg_active_coupon_style = '
.gb-active-coupon {
' . $coupon_style_attributes . '
}
';
// Add the above custom CSS via wp_add_inline_style.
wp_add_inline_style( 'smart-coupon', $gutenberg_active_coupon_style );
}
}
}
WC_SC_Gutenberg_Coupon_Block::get_instance();

View File

@@ -0,0 +1,120 @@
/**
* WooCommerce Smart Coupons Gutenberg
*
* @author StoreApps
* @since 4.0.0
* @version 1.0
*
* @package woocommerce-smart-coupons/includes/blocks
*/
( function( wp ) {
/**
* Registers a new block provided a unique name and an object defining its behavior.
*
* @see https://github.com/WordPress/gutenberg/tree/master/blocks#api
*/
const {registerBlockType} = wp.blocks;
/**
* Returns a new element of given type. Element is an abstraction layer atop React.
*
* @see https://github.com/WordPress/gutenberg/tree/master/element#element
*/
const createElement = wp.element.createElement;
/**
* Retrieves the translation of text.
*
* @see https://github.com/WordPress/gutenberg/tree/master/i18n#api
*/
const {__} = wp.i18n;
/**
* Every block starts by registering a new block type definition.
*
* @see https://wordpress.org/gutenberg/handbook/block-api/
*/
registerBlockType(
'woocommerce-smart-coupons/coupon',
{
/**
* This is the display title for our block, which can be translated with `i18n` functions.
* The block inserter will show this name.
*/
title: __( 'Smart Coupons', 'woocommerce-smart-coupons' ),
/**
* This is the block description for our block, which can be translated with `il8n` functions.
*/
description: __( 'Show any WooCommerce coupon with Smart Coupons.' ),
/**
* Blocks are grouped into categories to help users browse and discover them.
* The categories provided by core are `common`, `embed`, `formatting`, `layout` and `widgets`.
*/
category: 'embed',
/**
* Define block icon for our block.
*/
icon: 'heart',
/**
* This are the block keywords using which our block can be searched.
*/
keywords: [
__( 'Smart' ),
__( 'Coupons' ),
__( 'Store Credit' ),
],
/**
* Optional block extended support features.
*/
supports: {
// Removes support for an HTML mode.
html: false,
// align: [ 'wide', 'full' ],.
},
attributes: {
coupon_code: {
type: 'string',
},
},
/**
* The edit function describes the structure of your block in the context of the editor.
* This represents what the editor will render when the block is used.
*
* @see https://wordpress.org/gutenberg/handbook/block-edit-save/#edit
*
* @param {Object} [props] Properties passed from the editor.
* @return {Element} Element to render.
*/
edit: function( props ){
return createElement(
wp.editor.RichText,
{
className: 'coupon-container gb-active-coupon',
value: props.attributes.coupon_code,
onChange: function( coupon_code ) {
props.setAttributes( { coupon_code: coupon_code } );
}
}
);
},
/**
* The save function defines the way in which the different attributes should be combined
* into the final markup, which is then serialized by Gutenberg into `post_content`.
*
* @see https://wordpress.org/gutenberg/handbook/block-edit-save/#save
*
* @return {Element} Element to render.
*/
save:function( props ){
return null;
}
}
);
} )( window.wp );

View File

@@ -0,0 +1,273 @@
<?php
/**
* Smart Coupons Initialize
*
* @author StoreApps
* @since 3.3.0
* @version 1.4.0
* @package WooCommerce Smart Coupons
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
if ( ! class_exists( 'WC_SC_Act_Deact' ) ) {
/**
* Class for handling actions to be performed during initialization
*/
class WC_SC_Act_Deact {
/**
* Database changes required for Smart Coupons
*
* Add option 'smart_coupon_email_subject' if not exists
* Enable 'Auto Generation' for Store Credit (discount_type: 'smart_coupon') not having any customer_email
* Disable 'apply_before_tax' for all Store Credit (discount_type: 'smart_coupon')
*/
public static function smart_coupon_activate() {
set_transient( '_smart_coupons_process_activation', 1, 30 );
if ( ! is_network_admin() && ! isset( $_GET['activate-multi'] ) ) { // phpcs:ignore
set_transient( '_smart_coupons_activation_redirect', 1, 30 );
}
}
/**
* Process activation
*/
public static function process_activation() {
global $wpdb, $blog_id, $woocommerce_smart_coupon;
$is_migrate_site_options = get_site_option( 'wc_sc_migrate_site_options', 'yes' );
if ( 'yes' === $is_migrate_site_options ) {
// phpcs:disable
$default_options = array(
'all_tasks_count_woo_sc', // => '',.
'bulk_coupon_action_woo_sc', // => '',.
'current_time_woo_sc', // => '',.
'remaining_tasks_count_woo_sc', // => '',.
'smart_coupons_combine_emails', // => 'no',.
'smart_coupons_is_send_email', // => 'yes',.
'start_time_woo_sc', // => '',.
'wc_sc_auto_apply_coupon_ids', // => maybe_serialize( array() ),.
'wc_sc_is_show_terms_notice', // => 'yes',.
'wc_sc_terms_page_id', // => '',.
'woocommerce_wc_sc_combined_email_coupon_settings', // => maybe_serialize( array() ),.
'woocommerce_wc_sc_email_coupon_settings', // => maybe_serialize( array() ),.
'woo_sc_action_data', // => '',.
'woo_sc_generate_coupon_posted_data', // => '',.
);
// phpcs:enable
$existing_options = array();
foreach ( $default_options as $option_name ) {
$site_option = get_site_option( $option_name );
if ( ! empty( $site_option ) ) {
$existing_options[ $option_name ] = $site_option;
}
}
}
if ( is_multisite() ) {
$blog_ids = $wpdb->get_col( "SELECT blog_id FROM {$wpdb->blogs}", 0 ); // WPCS: cache ok, db call ok.
} else {
$blog_ids = array( $blog_id );
}
if ( ! get_option( 'smart_coupon_email_subject' ) ) {
add_option( 'smart_coupon_email_subject' );
}
if ( ! is_object( $woocommerce_smart_coupon ) || ! is_a( $woocommerce_smart_coupon, 'WC_Smart_Coupons' ) || ! is_callable( array( $woocommerce_smart_coupon, 'update_post_meta' ) ) ) {
if ( ! class_exists( 'WC_Smart_Coupons' ) ) {
if ( file_exists( WP_PLUGIN_DIR . '/woocommerce-smart-coupons/includes/class-wc-smart-coupons.php' ) ) {
include_once WP_PLUGIN_DIR . '/woocommerce-smart-coupons/includes/class-wc-smart-coupons.php';
}
if ( class_exists( 'WC_Smart_Coupons' ) && is_callable( array( 'WC_Smart_Coupons', 'get_instance' ) ) ) {
$woocommerce_smart_coupon = WC_Smart_Coupons::get_instance();
}
}
}
if ( is_multisite() ) {
require_once ABSPATH . WPINC . '/ms-blogs.php';
}
foreach ( $blog_ids as $blogid ) {
if ( ( file_exists( WP_PLUGIN_DIR . '/woocommerce/woocommerce.php' ) ) && ( is_plugin_active( 'woocommerce/woocommerce.php' ) ) ) {
if ( is_multisite() ) {
switch_to_blog( $blogid );
}
$results = $wpdb->get_col(
$wpdb->prepare(
"SELECT postmeta.post_id FROM {$wpdb->prefix}postmeta as postmeta WHERE postmeta.meta_key = %s AND postmeta.meta_value = %s AND postmeta.post_id IN
(SELECT p.post_id FROM {$wpdb->prefix}postmeta AS p WHERE p.meta_key = %s AND p.meta_value = %s) ",
'discount_type',
'smart_coupon',
'customer_email',
'a:0:{}'
)
); // WPCS: cache ok, db call ok.
foreach ( $results as $result ) {
if ( is_object( $woocommerce_smart_coupon ) && is_callable( array( $woocommerce_smart_coupon, 'update_post_meta' ) ) ) {
$woocommerce_smart_coupon->update_post_meta( $result, 'auto_generate_coupon', 'yes' );
} else {
update_post_meta( $result, 'auto_generate_coupon', 'yes' );
}
}
// To disable apply_before_tax option for Gift Certificates / Store Credit.
$tax_post_ids = $wpdb->get_col( $wpdb->prepare( "SELECT post_id FROM {$wpdb->prefix}postmeta WHERE meta_key = %s AND meta_value = %s", 'discount_type', 'smart_coupon' ) ); // WPCS: cache ok, db call ok.
foreach ( $tax_post_ids as $tax_post_id ) {
if ( is_object( $woocommerce_smart_coupon ) && is_callable( array( $woocommerce_smart_coupon, 'update_post_meta' ) ) ) {
$woocommerce_smart_coupon->update_post_meta( $tax_post_id, 'apply_before_tax', 'no' );
} else {
update_post_meta( $tax_post_id, 'apply_before_tax', 'no' );
}
}
if ( 'yes' === $is_migrate_site_options ) {
$results = $wpdb->get_results( // phpcs:ignore
$wpdb->prepare(
"SELECT option_id,
option_name,
option_value
FROM {$wpdb->prefix}options
WHERE option_name IN (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)",
'smart_coupons_is_send_email',
'smart_coupons_combine_emails',
'woocommerce_wc_sc_combined_email_coupon_settings',
'woocommerce_wc_sc_email_coupon_settings',
'wc_sc_is_show_terms_notice',
'wc_sc_terms_page_id',
'woo_sc_generate_coupon_posted_data',
'start_time_woo_sc',
'current_time_woo_sc',
'all_tasks_count_woo_sc',
'remaining_tasks_count_woo_sc',
'bulk_coupon_action_woo_sc',
'woo_sc_action_data',
'wc_sc_auto_apply_coupon_ids'
),
ARRAY_A
);
if ( ! empty( $results ) ) {
foreach ( $results as $result ) {
$option_value = ( ! empty( $existing_options[ $result['option_name'] ] ) ) ? $existing_options[ $result['option_name'] ] : '';
if ( ! empty( $option_value ) ) {
if ( is_array( $option_value ) ) {
$option_value = maybe_serialize( $option_value );
}
$wpdb->query( // phpcs:ignore
$wpdb->prepare(
"INSERT INTO {$wpdb->prefix}options (option_id, option_name, option_value, autoload)
VALUES (%d,%s,%s,%s)
ON DUPLICATE KEY UPDATE option_value = %s",
$result['option_id'],
$result['option_name'],
$option_value,
'no',
$option_value
)
);
}
}
}
}
if ( is_multisite() ) {
restore_current_blog(); // phpcs:ignore
}
}
}
if ( 'yes' === $is_migrate_site_options ) {
update_site_option( 'wc_sc_migrate_site_options', 'no' );
}
$woocommerce_smart_coupon->maybe_sync_orders_prior_to_800();
}
/**
* Database changes required for Smart Coupons
*
* Delete option 'sc_display_global_coupons' if exists
*/
public static function smart_coupon_deactivate() {
global $woocommerce_smart_coupon;
if ( get_option( 'sc_display_global_coupons' ) !== false ) {
delete_option( 'sc_display_global_coupons' );
}
if ( false === ( get_option( 'sc_flushed_rules' ) ) || 'found' === ( get_option( 'sc_flushed_rules' ) ) ) {
delete_option( 'sc_flushed_rules' );
}
self::clear_cache();
if ( ! is_object( $woocommerce_smart_coupon ) || ! is_a( $woocommerce_smart_coupon, 'WC_Smart_Coupons' ) || ! is_callable( array( $woocommerce_smart_coupon, 'update_post_meta' ) ) ) {
if ( ! class_exists( 'WC_Smart_Coupons' ) ) {
if ( file_exists( WP_PLUGIN_DIR . '/woocommerce-smart-coupons/includes/class-wc-smart-coupons.php' ) ) {
include_once WP_PLUGIN_DIR . '/woocommerce-smart-coupons/includes/class-wc-smart-coupons.php';
}
if ( class_exists( 'WC_Smart_Coupons' ) && is_callable( array( 'WC_Smart_Coupons', 'get_instance' ) ) ) {
$woocommerce_smart_coupon = WC_Smart_Coupons::get_instance();
}
}
}
$woocommerce_smart_coupon->record_latest_800_order();
}
/**
* Clear cache
*
* @return string $message
*/
public static function clear_cache() {
$all_cache_key = get_option( 'wc_sc_all_cache_key' );
if ( ! empty( $all_cache_key ) ) {
$cleared_cache = array();
foreach ( $all_cache_key as $key ) {
$is_cleared = wp_cache_delete( $key );
if ( true === $is_cleared ) {
$cleared_cache[] = $key;
}
}
// phpcs:disable
// if ( count( $all_cache_key ) === count( $cleared_cache ) ) {
// update_option( 'wc_sc_all_cache_key', array(), 'no' );
// /* translators: Number of all cache key */
// $message = sprintf( __( 'Successfully cleared %d cache!', 'woocommerce-smart-coupons' ), count( $all_cache_key ) );
// } else {
// $remaining = array_diff( $all_cache_key, $cleared_cache );
// update_option( 'wc_sc_all_cache_key', $remaining, 'no' );
// /* translators: 1. Number of cache not deleted 2. Number of all cache key */
// $message = sprintf( __( 'Failed! Could not clear %1$d out of %2$d cache', 'woocommerce-smart-coupons' ), count( $remaining ), count( $all_cache_key ) );
// }
// phpcs:enable
// There's no method in WordPress to detect if the cache is added successfully.
// Therefore it's not possible to know whether it is deleted or not.
// Hence returning a success message always.
// Enable above block of code when we can detect saving & deleting of cache.
}
return __( 'Successfully cleared WooCommerce Smart Coupons cache!', 'woocommerce-smart-coupons' );
}
}
}

View File

@@ -0,0 +1,426 @@
<?php
/**
* Actions for coupons dashboard
*
* @author StoreApps
* @since 4.4.0
* @version 1.5.0
* @package WooCommerce Smart Coupons
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
if ( ! class_exists( 'WC_SC_Admin_Coupons_Dashboard_Actions' ) ) {
/**
* Class for actions on coupons dashboard
*/
class WC_SC_Admin_Coupons_Dashboard_Actions {
/**
* Variable to hold instance of WC_SC_Admin_Coupons_Dashboard_Actions
*
* @var $instance
*/
private static $instance = null;
/**
* Constructor
*/
private function __construct() {
add_filter( 'post_row_actions', array( $this, 'woocommerce_sc_add_custom_actions' ), 1, 2 );
add_action( 'admin_action_duplicate_coupon', array( $this, 'woocommerce_duplicate_coupon_action' ) );
add_action( 'admin_footer', array( $this, 'add_post_row_script' ) );
}
/**
* Handle call to functions which is not available in this class
*
* @param string $function_name Function to call.
* @param array $arguments Array of arguments passed while calling $function_name.
* @return mixed Result of function call.
*/
public function __call( $function_name, $arguments = array() ) {
global $woocommerce_smart_coupon;
if ( ! is_callable( array( $woocommerce_smart_coupon, $function_name ) ) ) {
return;
}
if ( ! empty( $arguments ) ) {
return call_user_func_array( array( $woocommerce_smart_coupon, $function_name ), $arguments );
} else {
return call_user_func( array( $woocommerce_smart_coupon, $function_name ) );
}
}
/**
* Get single instance of WC_SC_Admin_Coupons_Dashboard_Actions
*
* @return WC_SC_Admin_Coupons_Dashboard_Actions Singleton object of WC_SC_Admin_Coupons_Dashboard_Actions
*/
public static function get_instance() {
// Check if instance is already exists.
if ( is_null( self::$instance ) ) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Add Smart Coupons actions on WC Coupons dashboard
*
* @param array $actions Array of existing actions.
* @param WP_Post $post Post object.
* @return array $actions including duplicate action of coupons
*/
public function woocommerce_sc_add_custom_actions( $actions, $post ) {
if ( 'shop_coupon' !== $post->post_type ) {
return $actions;
}
if ( ! current_user_can( 'manage_woocommerce' ) ) {
return $actions;
}
$coupon_code = ( ! empty( $post->post_title ) ) ? $post->post_title : '';
$coupon_id = ( ! empty( $post->ID ) ) ? $post->ID : 0;
$shop_page_id = get_option( 'woocommerce_shop_page_id', 0 );
if ( ! empty( $shop_page_id ) ) {
$shop_page_id = 'shop';
} else {
$home_url = home_url();
$shop_page_id = ( function_exists( 'wpcom_vip_url_to_postid' ) ) ? wpcom_vip_url_to_postid( $home_url ) : url_to_postid( $home_url ); // phpcs:ignore WordPressVIPMinimum.Functions.RestrictedFunctions.url_to_postid_url_to_postid
}
if ( empty( $shop_page_id ) ) {
$shop_page_id = 'cart';
}
$coupon_share_url = add_query_arg(
array(
'coupon-code' => $coupon_code,
'sc-page' => $shop_page_id,
),
home_url( '/' )
);
$actions['copy'] = '<a href="#" id="sc-click-to-copy-' . esc_attr( $coupon_id ) . '" onclick="sc_copy_to_clipboard(' . "'" . esc_js( $coupon_code ) . "'" . ')" data-clipboard-action="copy" data-clipboard-target=".row-title" title="' . __( 'Copy this coupon code', 'woocommerce-smart-coupons' ) . '" rel="permalink">' . __( 'Copy', 'woocommerce-smart-coupons' ) . '</a>';
$actions['share-link'] = '<a href="#" id="sc-click-to-share-' . esc_attr( $coupon_id ) . '" onclick="sc_copy_to_clipboard(' . "'" . esc_js( $coupon_share_url ) . "'" . ')" data-clipboard-action="copy" data-clipboard-target=".row-title" title="' . __( 'Copy coupon shareable link and apply via URL', 'woocommerce-smart-coupons' ) . '" rel="permalink">' . __( 'Get shareable link', 'woocommerce-smart-coupons' ) . '</a>';
if ( function_exists( 'duplicate_post_plugin_activation' ) ) {
return $actions;
} else {
$actions['duplicate'] = '<a href="' . esc_url( wp_nonce_url( admin_url( 'admin.php?action=duplicate_coupon&amp;post=' . $coupon_id ), 'woocommerce-duplicate-coupon_' . $coupon_id ) ) . '" title="' . __( 'Make a duplicate from this coupon', 'woocommerce-smart-coupons' ) . '" rel="permalink">' . __( 'Duplicate', 'woocommerce-smart-coupons' ) . '</a>';
}
return $actions;
}
/**
* Function to insert post meta values for duplicate coupon
*
* @param int $id ID of parent coupon.
* @param int $new_id ID of duplicated coupon.
*/
public function woocommerce_duplicate_coupon_post_meta( $id, $new_id ) {
global $wpdb;
$meta_keys = array( 'expiry_date', 'usage_count', '_used_by', 'date_expires' );
$how_many = count( $meta_keys );
$placeholders = array_fill( 0, $how_many, '%s' );
$post_meta_infos = $wpdb->get_results( $wpdb->prepare( "SELECT meta_key, meta_value FROM $wpdb->postmeta WHERE post_id=%d AND meta_key NOT IN ( " . implode( ',', $placeholders ) . ' )', array_merge( array( $id ), $meta_keys ) ) ); // phpcs:ignore
if ( 0 !== count( $post_meta_infos ) ) {
$sql_query = "INSERT INTO $wpdb->postmeta (post_id, meta_key, meta_value) ";
foreach ( $post_meta_infos as $meta_info ) {
$meta_key = $meta_info->meta_key;
$meta_value = $meta_info->meta_value;
$sql_query_sel[] = $wpdb->prepare( 'SELECT %d, %s, %s', $new_id, $meta_key, $meta_value );
}
$sql_query .= implode( ' UNION ALL ', $sql_query_sel );
$wpdb->query( $sql_query ); // phpcs:ignore
}
}
/**
* Function to duplicate post taxonomies for the duplicate coupon
*
* @param int $id ID of parent coupon.
* @param int $new_id ID of duplicated coupon.
* @param string $post_type Post type being duplicated.
*/
public function woocommerce_duplicate_coupon_post_taxonomies( $id, $new_id, $post_type ) {
$taxonomies = get_object_taxonomies( $post_type );
foreach ( $taxonomies as $taxonomy ) {
$post_terms = wp_get_object_terms( $id, $taxonomy );
$post_terms_count = count( $post_terms );
for ( $i = 0; $i < $post_terms_count; $i++ ) {
wp_set_object_terms( $new_id, $post_terms[ $i ]->slug, $taxonomy, true );
}
}
}
/**
* Function to create duplicate coupon and copy all properties of the coupon to duplicate coupon
*
* @param WP_Post $post Post object.
* @param int $parent Post parent ID.
* @param string $post_status Post status.
* @return int $new_post_id
*/
public function woocommerce_create_duplicate_from_coupon( $post, $parent = 0, $post_status = '' ) {
global $wpdb;
$new_post_author = wp_get_current_user();
$new_post_date = current_time( 'mysql' );
$new_post_date_gmt = get_gmt_from_date( $new_post_date );
if ( $parent > 0 ) {
$post_parent = $parent;
$post_status = $post_status ? $post_status : 'publish';
$suffix = '';
} else {
$post_parent = $post->post_parent;
$post_status = $post_status ? $post_status : 'draft';
$suffix = __( '(Copy)', 'woocommerce-smart-coupons' );
}
$new_post_type = $post->post_type;
$post_content = str_replace( "'", "''", $post->post_content );
$post_content_filtered = str_replace( "'", "''", $post->post_content_filtered );
$post_excerpt = str_replace( "'", "''", $post->post_excerpt );
$post_title = strtolower( str_replace( "'", "''", $post->post_title ) . $suffix );
$post_name = str_replace( "'", "''", $post->post_name );
$comment_status = str_replace( "'", "''", $post->comment_status );
$ping_status = str_replace( "'", "''", $post->ping_status );
$wpdb->insert(
$wpdb->posts,
array(
'post_author' => $new_post_author->ID,
'post_date' => $new_post_date,
'post_date_gmt' => $new_post_date_gmt,
'post_content' => $post_content,
'post_content_filtered' => $post_content_filtered,
'post_title' => $post_title,
'post_excerpt' => $post_excerpt,
'post_status' => $post_status,
'post_type' => $new_post_type,
'comment_status' => $comment_status,
'ping_status' => $ping_status,
'post_password' => $post->post_password,
'to_ping' => $post->to_ping,
'pinged' => $post->pinged,
'post_modified' => $new_post_date,
'post_modified_gmt' => $new_post_date_gmt,
'post_parent' => 0, // No need to link it with any other coupon.
'menu_order' => $post->menu_order,
'post_mime_type' => $post->post_mime_type,
)
); // WPCS: db call ok.
$new_post_id = $wpdb->insert_id;
$this->woocommerce_duplicate_coupon_post_taxonomies( $post->ID, $new_post_id, $post->post_type );
$this->woocommerce_duplicate_coupon_post_meta( $post->ID, $new_post_id );
return $new_post_id;
}
/**
* Function to return post id of the duplicate coupon to be created
*
* @param int $id ID of the coupon to duplicate.
* @return object $post Duplicated post object.
*/
public function woocommerce_get_coupon_to_duplicate( $id ) {
global $wpdb;
$post = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM $wpdb->posts WHERE ID=%d", $id ) ); // WPCS: cache ok, db call ok.
if ( isset( $post->post_type ) && 'revision' === $post->post_type ) {
$id = $post->post_parent;
$post = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM $wpdb->posts WHERE ID=%d", $id ) ); // WPCS: cache ok, db call ok.
}
return $post[0];
}
/**
* Function to validate condition and create duplicate coupon
*/
public function woocommerce_duplicate_coupon() {
if ( empty( $_REQUEST['post'] ) || ( isset( $_REQUEST['action'] ) && 'duplicate_post_save_as_new_page' === $_REQUEST['action'] ) ) { // phpcs:ignore
wp_die( esc_html__( 'No coupon to duplicate has been supplied!', 'woocommerce-smart-coupons' ) );
}
// Get the original page.
$id = absint( $_REQUEST['post'] ); // WPCS: input var ok.
check_admin_referer( 'woocommerce-duplicate-coupon_' . $id );
$post = $this->woocommerce_get_coupon_to_duplicate( $id );
if ( isset( $post ) && null !== $post ) {
$new_id = $this->woocommerce_create_duplicate_from_coupon( $post );
// If you have written a plugin which uses non-WP database tables to save
// information about a page you can hook this action to dupe that data.
do_action( 'woocommerce_duplicate_coupon', $new_id, $post );
// Redirect to the edit screen for the new draft page.
wp_safe_redirect( admin_url( 'post.php?action=edit&post=' . $new_id ) );
exit;
} else {
/* translators: %d: Post ID */
wp_die( sprintf( esc_html__( 'Coupon creation failed, could not find original coupon: %d', 'woocommerce-smart-coupons' ), esc_html( $id ) ) );
}
}
/**
* Function to call function to create duplicate coupon
*/
public function woocommerce_duplicate_coupon_action() {
$coupon_id = ( ! empty( $_REQUEST['post'] ) ) ? absint( $_REQUEST['post'] ) : 0;
check_admin_referer( 'woocommerce-duplicate-coupon_' . $coupon_id );
$action = ( ! empty( $_REQUEST['action'] ) ) ? wc_clean( wp_unslash( $_REQUEST['action'] ) ) : ''; // phpcs:ignore
if ( 'duplicate_coupon' !== $action ) {
return;
}
if ( $this->is_wc_gte_30() ) {
$coupon = new WC_Coupon( $coupon_id );
if ( false === $coupon ) {
/* translators: %s: coupon id */
wp_die( sprintf( esc_html__( 'Coupon creation failed, could not find original coupon: %s', 'woocommerce-smart-coupons' ), esc_html( $coupon_id ) ) );
}
$duplicate = $this->coupon_duplicate( $coupon );
// Hook rename to match other woocommerce_coupon_* hooks, and to move away from depending on a response from the wp_posts table.
do_action( 'wc_sc_duplicate_coupon', $duplicate, $coupon );
// Redirect to the edit screen for the new draft page.
wp_safe_redirect( admin_url( 'post.php?action=edit&post=' . $duplicate->get_id() ) );
exit;
} else {
$this->woocommerce_duplicate_coupon();
}
}
/**
* Duplicate coupon
*
* @param WC_Coupon $coupon The coupon object to duplicate.
* @return WC_Coupon $duplicate The duplicated coupon.
*/
public function coupon_duplicate( $coupon = null ) {
/**
* Filter to allow us to exclude meta keys from coupon duplication..
*
* @param array $exclude_meta The keys to exclude from the duplicate.
* @param array $existing_meta_keys The meta keys that the coupon already has.
* @since 7.2.0
*/
$meta_to_exclude = array_filter(
apply_filters(
'wc_sc_duplicate_coupon_exclude_meta',
array(),
array_map(
function ( $meta ) {
return $meta->key;
},
$coupon->get_meta_data()
)
)
);
$duplicate = clone $coupon;
$duplicate->set_id( 0 );
/* translators: %s contains the code of the original coupon. */
$duplicate->set_code( sprintf( '%s-copy', $duplicate->get_code() ) );
$duplicate->set_date_created( null );
$duplicate->set_usage_count( 0 );
$duplicate->set_used_by( array() );
$duplicate->set_date_expires( null );
if ( $this->is_wc_greater_than( '6.1.2' ) && $this->is_callable( $duplicate, 'set_status' ) ) {
$duplicate->set_status( 'draft' );
}
foreach ( $meta_to_exclude as $meta_key ) {
$duplicate->delete_meta_data( $meta_key );
}
/**
* This action can be used to modify the object further before it is created - it will be passed by reference.
*
* @since 3.0
*/
do_action( 'wc_sc_coupon_duplicate_before_save', $duplicate, $coupon );
// Save parent coupon.
$duplicate_id = $duplicate->save();
$duplicate = new WC_Coupon( $duplicate );
$this->woocommerce_duplicate_coupon_post_taxonomies( $coupon->get_id(), $duplicate_id, 'shop_coupon' );
if ( $this->is_wc_greater_than( '6.1.2' ) && $this->is_callable( $duplicate, 'get_status' ) ) {
$coupon_status = $duplicate->get_status();
} else {
$coupon_status = get_post_status( $duplicate_id );
}
if ( ! empty( $duplicate_id ) && 'draft' !== $coupon_status ) {
$args = array(
'ID' => $duplicate_id,
'post_status' => 'draft',
);
wp_update_post( $args ); // Because $coupon->set_status( 'draft' ) not working.
}
return new WC_Coupon( $duplicate_id );
}
/**
* Function to copy and share coupon via jQuery.
*/
public function add_post_row_script() {
$screen = get_current_screen();
if ( 'edit-shop_coupon' === $screen->id ) {
?>
<script type="text/javascript" class="sc-copy-share">
function sc_copy_to_clipboard(copyElement) {
var copyText = copyElement;
var temp = jQuery('<input>');
jQuery("body").append(temp);
temp.val(copyText).select();
document.execCommand("copy");
temp.remove();
}
</script>
<?php
}
}
}
}
WC_SC_Admin_Coupons_Dashboard_Actions::get_instance();

View File

@@ -0,0 +1,485 @@
<?php
/**
* Smart Coupons Admin Notifications
*
* @author StoreApps
* @since 4.0.0
* @version 1.6.0
*
* @package woocommerce-smart-coupons/includes/
*/
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
if ( ! class_exists( 'WC_SC_Admin_Notifications' ) ) {
/**
* Class for handling admin pages of Smart Coupons
*/
class WC_SC_Admin_Notifications {
/**
* Variable to hold instance of WC_SC_Admin_Notifications
*
* @var $instance
*/
private static $instance = null;
/**
* Constructor
*/
private function __construct() {
add_filter( 'plugin_action_links_' . plugin_basename( WC_SC_PLUGIN_FILE ), array( $this, 'plugin_action_links' ) );
add_action( 'wp_ajax_wc_sc_review_notice_action', array( $this, 'wc_sc_review_notice_action' ) );
add_action( 'wp_ajax_wc_sc_40_notice_action', array( $this, 'wc_sc_40_notice_action' ) );
add_action( 'admin_notices', array( $this, 'show_plugin_notice' ) );
// To update footer text on SC screens.
add_filter( 'admin_footer_text', array( $this, 'wc_sc_footer_text' ) );
add_filter( 'update_footer', array( $this, 'wc_sc_update_footer_text' ), 99 );
// To show 'Connect your store' notice of WC Helper on SC pages.
add_filter( 'woocommerce_screen_ids', array( $this, 'add_wc_connect_store_notice_on_sc_pages' ) );
// Show Database update notices.
add_action( 'admin_notices', array( $this, 'admin_db_update_notices' ) );
}
/**
* Get single instance of WC_SC_Admin_Pages
*
* @return WC_SC_Admin_Pages Singleton object of WC_SC_Admin_Pages
*/
public static function get_instance() {
// Check if instance is already exists.
if ( is_null( self::$instance ) ) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Handle call to functions which is not available in this class
*
* @param string $function_name The function name.
* @param array $arguments Array of arguments passed while calling $function_name.
* @return result of function call
*/
public function __call( $function_name, $arguments = array() ) {
global $woocommerce_smart_coupon;
if ( ! is_callable( array( $woocommerce_smart_coupon, $function_name ) ) ) {
return;
}
if ( ! empty( $arguments ) ) {
return call_user_func_array( array( $woocommerce_smart_coupon, $function_name ), $arguments );
} else {
return call_user_func( array( $woocommerce_smart_coupon, $function_name ) );
}
}
/**
* Function to add more action on plugins page
*
* @param array $links Existing links.
* @return array $links
*/
public function plugin_action_links( $links ) {
$action_links = array(
'settings' => '<a href="' . esc_url( admin_url( 'admin.php?page=wc-settings&tab=wc-smart-coupons' ) ) . '">' . esc_html__( 'Settings', 'woocommerce-smart-coupons' ) . '</a>',
'faqs' => '<a href="' . esc_url( admin_url( 'admin.php?page=sc-faqs' ) ) . '">' . esc_html__( 'FAQ\'s', 'woocommerce-smart-coupons' ) . '</a>',
'docs' => '<a target="_blank" href="' . esc_url( 'https://woocommerce.com/document/smart-coupons/' ) . '">' . __( 'Docs', 'woocommerce-smart-coupons' ) . '</a>',
'support' => '<a target="_blank" href="' . esc_url( 'https://woocommerce.com/my-account/create-a-ticket/' ) . '">' . __( 'Support', 'woocommerce-smart-coupons' ) . '</a>',
'review' => '<a target="_blank" href="' . esc_url( 'https://woocommerce.com/products/smart-coupons/?review' ) . '">' . __( 'Review', 'woocommerce-smart-coupons' ) . '</a>',
);
return array_merge( $action_links, $links );
}
/**
* Handle Smart Coupons review notice action
*/
public function wc_sc_review_notice_action() {
check_ajax_referer( 'wc-sc-review-notice-action', 'security' );
$post_do = ( ! empty( $_POST['do'] ) ) ? wc_clean( wp_unslash( $_POST['do'] ) ) : ''; // phpcs:ignore
$option = strtotime( '+1 month' );
if ( 'remove' === $post_do ) {
$option = 'no';
}
update_option( 'wc_sc_is_show_review_notice', $option, 'no' );
wp_send_json( array( 'success' => 'yes' ) );
}
/**
* Handle Smart Coupons version 4.0.0 notice action
*/
public function wc_sc_40_notice_action() {
check_ajax_referer( 'wc-sc-40-notice-action', 'security' );
update_option( 'wc_sc_is_show_40_notice', 'no', 'no' );
wp_send_json( array( 'success' => 'yes' ) );
}
/**
* Show plugin review notice
*/
public function show_plugin_notice() {
global $pagenow, $post;
$valid_post_types = array( 'shop_coupon', 'shop_order', 'product' );
$valid_pagenow = array( 'edit.php', 'post.php', 'plugins.php' );
$is_show_review_notice = get_option( 'wc_sc_is_show_review_notice' );
$is_coupon_enabled = get_option( 'woocommerce_enable_coupons' );
$get_post_type = ( ! empty( $post->ID ) ) ? $this->get_post_type( $post->ID ) : '';
$get_page = ( ! empty( $_GET['page'] ) ) ? wc_clean( wp_unslash( $_GET['page'] ) ) : ''; // phpcs:ignore
$get_tab = ( ! empty( $_GET['tab'] ) ) ? wc_clean( wp_unslash( $_GET['tab'] ) ) : ''; // phpcs:ignore
$design = get_option( 'wc_sc_setting_coupon_design', 'basic' );
$is_page = ( in_array( $pagenow, $valid_pagenow, true ) || in_array( $get_post_type, $valid_post_types, true ) || ( 'admin.php' === $pagenow && ( 'wc-smart-coupons' === $get_page || 'wc-smart-coupons' === $get_tab ) ) );
if ( $is_page && 'yes' !== $is_coupon_enabled ) {
?>
<div id="wc_sc_coupon_disabled" class="updated fade error">
<p>
<?php
echo '<strong>' . esc_html__( 'Important', 'woocommerce-smart-coupons' ) . ':</strong> ' . esc_html__( 'Setting "Enable the use of coupon codes" is disabled.', 'woocommerce-smart-coupons' ) . ' ' . sprintf(
'<a href="%s">%s</a>',
esc_url(
add_query_arg(
array(
'page' => 'wc-settings',
'tab' => 'general',
),
admin_url( 'admin.php' )
)
),
esc_html__( 'Enable', 'woocommerce-smart-coupons' )
) . ' ' . esc_html__( 'it to use', 'woocommerce-smart-coupons' ) . ' <strong>' . esc_html__( 'WooCommerce Smart Coupons', 'woocommerce-smart-coupons' ) . '</strong> ' . esc_html__( 'features.', 'woocommerce-smart-coupons' );
?>
</p>
</div>
<?php
}
// Review Notice.
if ( $is_page && ! empty( $is_show_review_notice ) && 'no' !== $is_show_review_notice && time() >= absint( $is_show_review_notice ) ) {
if ( ! wp_script_is( 'jquery' ) ) {
wp_enqueue_script( 'jquery' );
}
?>
<style type="text/css" media="screen">
#wc_sc_review_notice .wc_sc_review_notice_action {
float: right;
padding: 0.5em 0;
text-align: right;
}
</style>
<script type="text/javascript">
jQuery(function(){
jQuery('body').on('click', '#wc_sc_review_notice .wc_sc_review_notice_action a.wc_sc_review_notice_remind', function( e ){
jQuery.ajax({
url: '<?php echo esc_url( admin_url( 'admin-ajax.php' ) ); ?>',
type: 'post',
dataType: 'json',
data: {
action: 'wc_sc_review_notice_action',
security: '<?php echo esc_html( wp_create_nonce( 'wc-sc-review-notice-action' ) ); ?>',
do: 'remind'
},
success: function( response ){
if ( response.success != undefined && response.success != '' && response.success == 'yes' ) {
jQuery('#wc_sc_review_notice').fadeOut(500, function(){ jQuery('#wc_sc_review_notice').remove(); });
}
}
});
return false;
});
jQuery('body').on('click', '#wc_sc_review_notice .wc_sc_review_notice_action a.wc_sc_review_notice_remove', function(){
jQuery.ajax({
url: '<?php echo esc_url( admin_url( 'admin-ajax.php' ) ); ?>',
type: 'post',
dataType: 'json',
data: {
action: 'wc_sc_review_notice_action',
security: '<?php echo esc_html( wp_create_nonce( 'wc-sc-review-notice-action' ) ); ?>',
do: 'remove'
},
success: function( response ){
if ( response.success != undefined && response.success != '' && response.success == 'yes' ) {
jQuery('#wc_sc_review_notice').fadeOut(500, function(){ jQuery('#wc_sc_review_notice').remove(); });
}
}
});
return false;
});
});
</script>
<div id="wc_sc_review_notice" class="updated fade">
<div class="wc_sc_review_notice_action">
<a href="javascript:void(0)" class="wc_sc_review_notice_remind"><?php echo esc_html__( 'Remind me after a month', 'woocommerce-smart-coupons' ); ?></a><br>
<a href="javascript:void(0)" class="wc_sc_review_notice_remove"><?php echo esc_html__( 'Never show again', 'woocommerce-smart-coupons' ); ?></a>
</div>
<p>
<?php echo esc_html__( 'Awesome, you successfully auto-generated a coupon! Are you having a great experience with', 'woocommerce-smart-coupons' ) . ' <strong>' . esc_html__( 'WooCommerce Smart Coupons', 'woocommerce-smart-coupons' ) . '</strong> ' . esc_html__( 'so far?', 'woocommerce-smart-coupons' ) . '<br>' . esc_html__( 'Please consider', 'woocommerce-smart-coupons' ) . ' <a href="' . esc_url( 'https://woocommerce.com/products/smart-coupons/#reviews' ) . '">' . esc_html__( 'leaving a review', 'woocommerce-smart-coupons' ) . '</a> ' . esc_html__( '! If things aren\'t going quite as expected, we\'re happy to help -- please reach out to', 'woocommerce-smart-coupons' ) . ' <a href="' . esc_url( 'https://woocommerce.com/my-account/create-a-ticket/' ) . '">' . esc_html__( 'our support team', 'woocommerce-smart-coupons' ) . '</a>.'; ?>
</p>
</div>
<?php
}
if ( $is_page && 'custom-design' === $design ) {
?>
<div class="updated fade error" style="background-color: #f0fff0;">
<p>
<?php
echo sprintf(
/* translators: 1: WooCommerce Smart Coupons 2: Link for the Smart Coupons settings */
esc_html__( '%1$s: You are using a custom coupon style which is planned to be removed from the plugin in upcoming versions. New, improved styles & colors are added in the version 4.9.0. We would request you to choose a color scheme & a style for coupon from the newly added colors & styles. You can do this from %2$s.', 'woocommerce-smart-coupons' ),
'<strong>' . esc_html__( 'WooCommerce Smart Coupons', 'woocommerce-smart-coupons' ) . '</strong>',
'<a href="' . esc_url(
add_query_arg(
array(
'page' => 'wc-settings',
'tab' => 'wc-smart-coupons',
),
admin_url( 'admin.php' )
)
) . '" target="_blank">' . esc_html__(
'Smart Coupons settings',
'woocommerce-smart-coupons'
) . '</a>'
);
?>
</p>
</div>
<?php
}
}
/**
* Function to 'Connect your store' notice on Smart Coupons pages in admin
*
* @param string $sc_rating_text Text in footer (left).
* @return string $sc_rating_text
*/
public function wc_sc_footer_text( $sc_rating_text ) {
global $post, $pagenow;
if ( ! empty( $pagenow ) ) {
$get_post_type = ( ! empty( $post->ID ) ) ? $this->get_post_type( $post->ID ) : '';
$get_page = ( ! empty( $_GET['page'] ) ) ? wc_clean( wp_unslash( $_GET['page'] ) ) : ''; // phpcs:ignore
$get_tab = ( ! empty( $_GET['tab'] ) ) ? wc_clean( wp_unslash( $_GET['tab'] ) ) : ''; // phpcs:ignore
$sc_pages = array( 'wc-smart-coupons', 'sc-about', 'sc-faqs' );
if ( in_array( $get_page, $sc_pages, true ) || 'shop_coupon' === $get_post_type || 'wc-smart-coupons' === $get_tab ) {
?>
<style type="text/css">
#wpfooter {
display: block !important;
}
</style>
<?php
/* translators: %s: link to review WooCommerce Smart Coupons */
$sc_rating_text = wp_kses_post( sprintf( __( 'Liked WooCommerce Smart Coupons? Leave us a %s. A huge thank you from WooCommerce & StoreApps in advance!', 'woocommerce-smart-coupons' ), '<a target="_blank" href="' . esc_url( 'https://woocommerce.com/products/smart-coupons/?review' ) . '" style="color: #5850EC;">5-star rating here</a>' ) );
}
}
return $sc_rating_text;
}
/**
* Function to 'Connect your store' notice on Smart Coupons pages in admin
*
* @param string $sc_text Text in footer (right).
* @return string $sc_text
*/
public function wc_sc_update_footer_text( $sc_text ) {
global $post, $pagenow;
if ( ! empty( $pagenow ) ) {
$get_post_type = ( ! empty( $post->ID ) ) ? $this->get_post_type( $post->ID ) : '';
$get_page = ( ! empty( $_GET['page'] ) ) ? wc_clean( wp_unslash( $_GET['page'] ) ) : ''; // phpcs:ignore
$get_tab = ( ! empty( $_GET['tab'] ) ) ? wc_clean( wp_unslash( $_GET['tab'] ) ) : ''; // phpcs:ignore
$sc_pages = array( 'wc-smart-coupons', 'sc-about', 'sc-faqs' );
if ( in_array( $get_page, $sc_pages, true ) || 'shop_coupon' === $get_post_type || 'wc-smart-coupons' === $get_tab ) {
/* translators: %s: link to submit idea for Smart Coupons on WooCommerce idea board */
$sc_text = sprintf( __( 'Have a feature request? Submit it %s.', 'woocommerce-smart-coupons' ), '<a href="' . esc_url( 'https://woocommerce.com/feature-requests/smart-coupons/' ) . '" target="_blank" style="color: #5850EC;">' . __( 'here', 'woocommerce-smart-coupons' ) . '</a>' );
}
}
return $sc_text;
}
/**
* Function to 'Connect your store' notice on Smart Coupons pages in admin
*
* @param array $screen_ids List of existing screen ids.
* @return array $screen_ids
*/
public function add_wc_connect_store_notice_on_sc_pages( $screen_ids ) {
array_push( $screen_ids, 'woocommerce_page_wc-smart-coupons' );
return $screen_ids;
}
/**
* Function to render admin notice
*
* @param string $type Notice type.
* @param string $title Notice title.
* @param string $message Notice message.
* @param string $action Notice actions.
* @param bool $dismissible Notice dismissible.
* @return void.
*/
public function show_notice( $type = 'info', $title = '', $message = '', $action = '', $dismissible = false ) {
$css_classes = array(
'notice',
'notice-' . $type,
);
if ( true === $dismissible ) {
$css_classes[] = 'is-dismissible';
}
?>
<div class="<?php echo esc_attr( implode( ' ', $css_classes ) ); ?>">
<?php
if ( ! empty( $title ) ) {
printf( '<p><strong>%s</strong></p>', esc_html( $title ) );
}
if ( ! empty( $message ) ) {
printf( '<p>%s</p>', esc_html( $message ) );
}
if ( ! empty( $action ) ) {
printf( '<p class="submit">%s</p>', wp_kses_post( $action ) );
}
?>
</div>
<?php
}
/**
* Function to show database update notice
*/
public function admin_db_update_notices() {
if ( ! class_exists( 'WC_SC_Background_Upgrade' ) ) {
include_once 'class-wc-sc-background-upgrade.php';
}
$wcsc_db = WC_SC_Background_Upgrade::get_instance();
$update_status = $wcsc_db->get_status( '4.28.0' );
if ( 'pending' === $update_status ) {
// Notice for pending update.
$this->db_update_pending_notice();
} elseif ( 'processing' === $update_status ) {
// Notice for processing update.
$this->db_update_processing_notice();
} elseif ( 'completed' === $update_status ) {
// Notice for completed update.
$this->db_update_completed_notice();
$wcsc_db->set_status( '4.28.0', 'done' );
}
}
/**
* Function to show pending database update notice
*/
public function db_update_pending_notice() {
global $woocommerce_smart_coupon;
$plugin_version = $woocommerce_smart_coupon->get_smart_coupons_version();
/* translators: %s: Plugin name */
$title = sprintf( __( '%s database update required', 'woocommerce-smart-coupons' ), 'WooCommerce Smart Coupons' );
$message = __( 'The database update process runs in the background and may take a little while, so please be patient.', 'woocommerce-smart-coupons' );
$update_url = wp_nonce_url(
add_query_arg(
array(
'page' => 'wc-settings',
'tab' => 'wc-smart-coupons',
'wc_sc_update' => '4.28.0',
),
admin_url( 'admin.php' )
),
'wc_sc_db_process',
'wc_sc_db_update_nonce'
);
$action_button = sprintf( '<a href="%1$s" class="button button-primary">%2$s</a>', esc_url( $update_url ), __( 'Update database', 'woocommerce-smart-coupons' ) );
$this->show_notice( 'warning', $title, $message, $action_button );
}
/**
* Function to show database update processing notice.
*/
public function db_update_processing_notice() {
if ( 'woocommerce_page_wc-status' === $this->get_current_screen_id() && isset( $_GET['tab'] ) && 'action-scheduler' === wc_clean( wp_unslash( $_GET['tab'] ) ) ) { // phpcs:ignore
return;
}
$actions_url = add_query_arg(
array(
'page' => 'wc-status',
'tab' => 'action-scheduler',
's' => 'move_applied_coupon_options_to_transient',
'status' => 'pending',
),
admin_url( 'admin.php' )
);
$cron_disabled = defined( 'DISABLE_WP_CRON' ) && DISABLE_WP_CRON;
/* translators: %s: Plugin name */
$message = sprintf( __( '%s is updating the database in the background. The database update process may take a little while, so please be patient.', 'woocommerce-smart-coupons' ), 'WooCommerce Smart Coupons' );
if ( true === $cron_disabled ) {
$message .= '<br>' . __( 'Note: WP CRON has been disabled on your install which may prevent this update from completing.', 'woocommerce-smart-coupons' );
}
$action_button = sprintf( '<a href="%1$s" class="button button-secondary">%2$s</a>', esc_url( $actions_url ), __( 'View status', 'woocommerce-smart-coupons' ) );
$this->show_notice( 'info', '', $message, $action_button );
}
/**
* Function to show database update completed notice.
*/
public function db_update_completed_notice() {
/* translators: %s: Plugin name */
$message = sprintf( __( '%s database update completed. Thank you for updating to the latest version!', 'woocommerce-smart-coupons' ), 'WooCommerce Smart Coupons' );
$this->show_notice( 'success', '', $message, '', true );
}
/**
* Function to get current screen id.
*
* @return string.
*/
public function get_current_screen_id() {
$screen = get_current_screen();
return $screen ? $screen->id : '';
}
}
}
WC_SC_Admin_Notifications::get_instance();

View File

@@ -0,0 +1,504 @@
<?php
/**
* A Welcome page for store admin
*
* @author StoreApps
* @since 3.3.0
* @version 1.4.0
*
* @package woocommerce-smart-coupons/includes/
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
if ( ! class_exists( 'WC_SC_Admin_Welcome' ) ) {
/**
* WC_SC_Admin_Welcome class
*/
class WC_SC_Admin_Welcome {
/**
* Variable to hold instance of WC_SC_Admin_Welcome
*
* @var $instance
*/
private static $instance = null;
/**
* Hook in tabs.
*/
private function __construct() {
add_action( 'admin_menu', array( $this, 'admin_menus' ) );
add_action( 'admin_head', array( $this, 'admin_head' ) );
add_action( 'admin_init', array( $this, 'sc_welcome' ) );
}
/**
* Get single instance of WC_SC_Admin_Welcome
*
* @return WC_SC_Admin_Welcome Singleton object of WC_SC_Admin_Welcome
*/
public static function get_instance() {
// Check if instance is already exists.
if ( is_null( self::$instance ) ) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Handle call to functions which is not available in this class
*
* @param string $function_name The function name.
* @param array $arguments Array of arguments passed while calling $function_name.
* @return result of function call
*/
public function __call( $function_name, $arguments = array() ) {
global $woocommerce_smart_coupon;
if ( ! is_callable( array( $woocommerce_smart_coupon, $function_name ) ) ) {
return;
}
if ( ! empty( $arguments ) ) {
return call_user_func_array( array( $woocommerce_smart_coupon, $function_name ), $arguments );
} else {
return call_user_func( array( $woocommerce_smart_coupon, $function_name ) );
}
}
/**
* Add admin menus/screens.
*/
public function admin_menus() {
$get_page = ( ! empty( $_GET['page'] ) ) ? wc_clean( wp_unslash( $_GET['page'] ) ) : ''; // phpcs:ignore
if ( empty( $get_page ) ) {
return;
}
$welcome_page_name = __( 'About Smart Coupons', 'woocommerce-smart-coupons' );
$welcome_page_title = __( 'Welcome to Smart Coupons', 'woocommerce-smart-coupons' );
$parent_slug = ( $this->is_wc_gte_44() ) ? 'woocommerce-marketing' : 'woocommerce';
switch ( $get_page ) {
case 'sc-about':
add_submenu_page( $parent_slug, $welcome_page_title, $welcome_page_name, 'manage_options', 'sc-about', array( $this, 'about_screen' ) );
break;
case 'sc-faqs':
add_submenu_page( $parent_slug, $welcome_page_title, $welcome_page_name, 'manage_options', 'sc-faqs', array( $this, 'faqs_screen' ) );
break;
}
}
/**
* Add styles just for this page, and remove dashboard page links.
*/
public function admin_head() {
$parent_slug = ( $this->is_wc_gte_44() ) ? 'woocommerce-marketing' : 'woocommerce';
remove_submenu_page( $parent_slug, 'sc-about' );
remove_submenu_page( $parent_slug, 'sc-faqs' );
$get_page = ( ! empty( $_GET['page'] ) ) ? wc_clean( wp_unslash( $_GET['page'] ) ) : ''; // phpcs:ignore
if ( ! empty( $get_page ) && ( 'sc-faqs' === $get_page || 'sc-about' === $get_page ) ) {
?>
<style type="text/css">
/*<![CDATA[*/
.about-wrap h3 {
margin-top: 1em;
margin-right: 0em;
margin-bottom: 0.1em;
}
.about-wrap .button-primary {
margin-top: 18px;
}
.about-wrap .button-hero {
color: #FFF!important;
border-color: #03a025!important;
background: #03a025 !important;
box-shadow: 0 1px 0 #03a025;
font-size: 1em;
font-weight: bold;
}
.about-wrap .button-hero:hover {
color: #FFF!important;
background: #0AAB2E!important;
border-color: #0AAB2E!important;
}
.about-wrap p {
margin-top: 0.6em;
margin-bottom: 0.8em;
}
.about-wrap .feature-section {
padding-bottom: 5px;
}
.about-wrap .aligncenter {
text-align: center;
}
.about-wrap,
.about-wrap .has-2-columns,
.about-wrap .has-3-columns {
max-width: unset !important;
}
ul.sc-top-features {
list-style-type: disc !important;
padding-left: 1.5em !important;
}
/*]]>*/
</style>
<?php
}
}
/**
* Intro text/links shown on all about pages.
*/
private function intro() {
if ( is_callable( 'WC_Smart_Coupons::get_smart_coupons_plugin_data' ) ) {
$plugin_data = WC_Smart_Coupons::get_smart_coupons_plugin_data();
$version = $plugin_data['Version'];
} else {
$version = '';
}
?>
<h1><?php echo esc_html__( 'Thank you for installing WooCommerce Smart Coupons', 'woocommerce-smart-coupons' ) . ' ' . esc_html( $version ) . '!'; ?></h1>
<p class="about-text"><?php echo esc_html__( 'Glad to have you onboard. We hope WooCommerce Smart Coupons adds to your desired success 🏆', 'woocommerce-smart-coupons' ); ?></p>
<div class="has-2-columns feature-section col two-col" style="margin-bottom: 30px !important;">
<div class="is-vertically-aligned-center column col">
<a href="<?php echo esc_url( admin_url( 'edit.php?post_type=shop_coupon' ) ); ?>" class="button button-hero"><?php echo esc_html__( 'Go To Coupons', 'woocommerce-smart-coupons' ); ?></a>
</div>
<div class="is-vertically-aligned-center column col last-feature">
<p class="alignright">
<?php
$settings_tab_url = add_query_arg(
array(
'page' => 'wc-settings',
'tab' => 'wc-smart-coupons',
),
admin_url( 'admin.php' )
);
?>
<a href="<?php echo esc_url( $settings_tab_url ); ?>" class="button button-primary" target="_blank"><?php echo esc_html__( 'Settings', 'woocommerce-smart-coupons' ); ?></a>
<a href="<?php echo esc_url( apply_filters( 'smart_coupons_docs_url', 'https://woocommerce.com/document/smart-coupons/', 'woocommerce-smart-coupons' ) ); ?>" class="docs button button-primary" target="_blank"><?php echo esc_html__( 'Docs', 'woocommerce-smart-coupons' ); ?></a>
</p>
</div>
</div>
<h2 class="nav-tab-wrapper">
<a class="nav-tab
<?php
$get_page = ( ! empty( $_GET['page'] ) ) ? wc_clean( wp_unslash( $_GET['page'] ) ) : ''; // phpcs:ignore
if ( 'sc-about' === $get_page ) {
echo 'nav-tab-active';
}
?>
" href="<?php echo esc_url( admin_url( add_query_arg( array( 'page' => 'sc-about' ), 'admin.php' ) ) ); ?>">
<?php echo esc_html__( 'Know Smart Coupons', 'woocommerce-smart-coupons' ); ?>
</a>
<a class="nav-tab
<?php
if ( 'sc-faqs' === $get_page ) {
echo 'nav-tab-active';
}
?>
" href="<?php echo esc_url( admin_url( add_query_arg( array( 'page' => 'sc-faqs' ), 'admin.php' ) ) ); ?>">
<?php echo esc_html__( "FAQ's", 'woocommerce-smart-coupons' ); ?>
</a>
</h2>
<?php
}
/**
* Output the about screen.
*/
public function about_screen() {
if ( ! wp_script_is( 'jquery' ) ) {
wp_enqueue_script( 'jquery' );
}
?>
<script type="text/javascript">
jQuery(function(){
jQuery('#toplevel_page_woocommerce').find('a[href$=shop_coupon]').addClass('current');
jQuery('#toplevel_page_woocommerce').find('a[href$=shop_coupon]').parent().addClass('current');
});
</script>
<div class="wrap about-wrap">
<?php $this->intro(); ?>
<div>
<div class="has-3-columns feature-section col three-col">
<div class="column col">
<h4><?php echo esc_html__( 'What is Smart Coupons?', 'woocommerce-smart-coupons' ); ?></h4>
<p>
<?php echo esc_html__( 'Smart Coupons is a powerful extension, built on top of WooCommerce coupons. It adds a new discount type - Store Credit - and advanced functionality to the default coupons.', 'woocommerce-smart-coupons' ); ?>
<?php echo esc_html__( 'Smart Coupons enable coupons to become an automatic/interactive system.', 'woocommerce-smart-coupons' ); ?>
</p>
</div>
<div class="column col">
<h4><?php echo esc_html__( 'Top Smart Coupons features', 'woocommerce-smart-coupons' ); ?></h4>
<ul class="sc-top-features">
<li><?php echo esc_html__( 'Create and gift Store Credit / Gift Cards', 'woocommerce-smart-coupons' ); ?></li>
<li><?php echo esc_html__( 'Bulk generate coupons', 'woocommerce-smart-coupons' ); ?></li>
<li><?php echo esc_html__( 'Apply multiple coupons via URL', 'woocommerce-smart-coupons' ); ?></li>
<li><?php echo esc_html__( 'Advanced restrictions - payment, shipping, location, user roles, product attributes', 'woocommerce-smart-coupons' ); ?></li>
</ul>
<p>
<?php echo esc_html__( 'and a lot more…', 'woocommerce-smart-coupons' ); ?>
</p>
</div>
<div class="column col last-feature">
<h4><?php echo esc_html__( 'Automatic payment for subscription renewals', 'woocommerce-smart-coupons' ); ?></h4>
<p>
<?php
/* translators: WooCommerce Subscriptions product link */
echo sprintf( esc_html__( 'If your store is using %s and your customer has purchased a subscription using a Store Credit. If that store credit has balance left in it, store will automatically use it for renewing that subscription.', 'woocommerce-smart-coupons' ), '<a href="https://woocommerce.com/products/woocommerce-subscriptions/?aff=5475">' . esc_html__( 'WooCommerce Subscriptions', 'woocommerce-smart-coupons' ) . '</a>' );
?>
</p>
</div>
</div>
<center><h3><?php echo esc_html__( 'How to use Smart Coupons the best way', 'woocommerce-smart-coupons' ); ?></h3></center>
<div class="has-3-columns feature-section col three-col">
<div class="column col">
<h4><?php echo esc_html__( 'Sell or issue store credit / gift cards', 'woocommerce-smart-coupons' ); ?></h4>
<p>
<?php
/* translators: 1: Affiliate For WooCommerce 2: Smart Manager 3: Smart Offers */
echo sprintf( esc_html__( 'Let customers purchase gift cards from you or you issue store credit that your users can redeem on the current or next purchase. See how: %1$s or %2$s', 'woocommerce-smart-coupons' ), ' <a href="https://woocommerce.com/document/smart-coupons/how-to-sell-gift-card-of-any-amount/" target="_blank">' . esc_html__( 'any amount', 'woocommerce-smart-coupons' ) . '</a>', '<a href="https://woocommerce.com/document/smart-coupons/how-to-sell-gift-card-of-variable-but-a-fixed-amount/" target="_blank">' . esc_html__( 'variable but fixed amount', 'woocommerce-smart-coupons' ) . '</a>.' );
?>
</p>
</div>
<div class="column col">
<h4><?php echo esc_html__( 'Bulk create unique coupons & email them', 'woocommerce-smart-coupons' ); ?></h4>
<p>
<?php echo esc_html__( 'Import and export unique coupons in bulk via CSV. Share coupon codes to deal sites or email them to your customers.', 'woocommerce-smart-coupons' ) . ' <a href="https://woocommerce.com/document/smart-coupons/how-to-bulk-generate-coupons/" target="_blank">' . esc_html__( 'See how', 'woocommerce-smart-coupons' ) . '</a>.'; ?>
</p>
</div>
<div class="column col last-feature">
<h4><?php echo esc_html__( 'Gift a product via coupon', 'woocommerce-smart-coupons' ); ?></h4>
<p>
<?php echo esc_html__( 'Attach a gift of any value (free or paid product) to a particular coupon. Here, instead of a discount, a product is redeemed for the coupon code.', 'woocommerce-smart-coupons' ) . ' <a href="https://woocommerce.com/document/smart-coupons/how-to-gift-a-product-via-coupon/" target="_blank">' . esc_html__( 'See how', 'woocommerce-smart-coupons' ) . '</a>.'; ?>
</p>
</div>
</div>
<div class="has-3-columns feature-section col three-col">
<div class="column col">
<h4><?php echo esc_html__( 'Give discounts to customers for next purchase', 'woocommerce-smart-coupons' ); ?></h4>
<p>
<?php echo esc_html__( 'You can give a coupon to your customer after every purchase, which can encourage them to purchase again from you.', 'woocommerce-smart-coupons' ) . ' <a href="https://woocommerce.com/document/smart-coupons/how-to-give-coupons-with-products-for-next-order/" target="_blank">' . esc_html__( 'See how', 'woocommerce-smart-coupons' ) . '</a>.'; ?>
</p>
</div>
<div class="column col">
<h4><?php echo esc_html__( 'Set a maximum discount limit', 'woocommerce-smart-coupons' ); ?></h4>
<p>
<?php echo esc_html__( 'Give bigger discounts without hurting your profits. Offer a percentage off coupon upto a particular value. Example - Flat 50% off upto $100.', 'woocommerce-smart-coupons' ); ?>
</p>
</div>
<div class="column col last-feature">
<h4><?php echo esc_html__( 'Make customer\'s coupon usage, easy & simple', 'woocommerce-smart-coupons' ); ?></h4>
<p>
<?php echo esc_html__( 'Show only valid coupons to your customer (if logged in) on cart, checkout & My Account page. Coupons can be applied with single click. So, no need to remember the coupon code or copy-pasting.', 'woocommerce-smart-coupons' ); ?>
</p>
</div>
</div>
</div>
</div>
<?php
}
/**
* Output the FAQ's screen.
*/
public function faqs_screen() {
if ( ! wp_script_is( 'jquery' ) ) {
wp_enqueue_script( 'jquery' );
}
?>
<script type="text/javascript">
jQuery(function(){
jQuery('#toplevel_page_woocommerce').find('a[href$=shop_coupon]').addClass('current');
jQuery('#toplevel_page_woocommerce').find('a[href$=shop_coupon]').parent().addClass('current');
});
</script>
<div class="wrap about-wrap">
<?php $this->intro(); ?>
<h3 class="aligncenter"><?php echo esc_html__( 'FAQ / Common Problems', 'woocommerce-smart-coupons' ); ?></h3>
<?php
$faqs = array(
array(
'que' => esc_html__( 'When trying to add coupon/Smart Coupon, I get "Invalid post type" message.', 'woocommerce-smart-coupons' ),
'ans' => esc_html__( 'Make sure use of coupon is enabled in your store. You can find this setting', 'woocommerce-smart-coupons' ) . ' <a href="' . add_query_arg(
array(
'page' => 'wc-settings',
'tab' => 'general',
),
admin_url( 'admin.php' )
) . '" target="_blank">' . __( 'here', 'woocommerce-smart-coupons' ) . '</a>.',
),
array(
'que' => esc_html__( 'Smart Coupon\'s fields are broken?', 'woocommerce-smart-coupons' ),
'ans' => esc_html__( 'Make sure you are using the ', 'woocommerce-smart-coupons' ) . '<a target="_blank" href="http://dzv365zjfbd8v.cloudfront.net/changelogs/woocommerce-smart-coupons/changelog.txt">' . __( 'latest version of Smart Coupons', 'woocommerce-smart-coupons' ) . '</a>' . esc_html__( '. If still the issue persist, temporarily de-activate all plugins except WooCommerce & Smart Coupons. Re-check the issue, if the issue still persists, contact us (from the link at the end of this page). If the issue goes away, re-activate other plugins one-by-one & re-checking the fields, to find out which plugin is conflicting.', 'woocommerce-smart-coupons' ),
),
array(
'que' => esc_html__( 'How to translate texts from Smart Coupons?', 'woocommerce-smart-coupons' ),
'ans' => esc_html__( 'Simplest method is by installing', 'woocommerce-smart-coupons' ) . ' <a href="https://wordpress.org/plugins/loco-translate/" target="_blank">' . esc_html__( 'Loco Translate', 'woocommerce-smart-coupons' ) . '</a> ' . esc_html__( 'plugin and then following steps listed ', 'woocommerce-smart-coupons' ) . ' <a href="https://woocommerce.com/document/smart-coupons/how-to-translate-smart-coupons/" target="_blank">' . __( 'here', 'woocommerce-smart-coupons' ) . '</a>.',
),
array(
'que' => esc_html__( 'How to change texts of the emails sent from Smart Coupons?', 'woocommerce-smart-coupons' ),
'ans' => esc_html__( 'You can do this by overriding the email template.', 'woocommerce-smart-coupons' ) . ' <a href="https://woocommerce.com/document/smart-coupons/how-to-change-replace-and-override-email-template/" target="_blank">' . esc_html__( 'How to override email template', 'woocommerce-smart-coupons' ) . '</a>.',
),
array(
'que' => esc_html__( 'Can coupon code have any spaces in the name? / My Store Credit/Gift Certificate is not working (not generating new coupon code).', 'woocommerce-smart-coupons' ),
'ans' => esc_html__( 'No. Coupon code should not have any spaces in the name, Eg, Coupon code should be “gift-certificate” & not “gift certificate”.', 'woocommerce-smart-coupons' ),
),
array(
'que' => esc_html__( 'What\'s the URL to a coupon, so it\'s automatically inserted when visiting?', 'woocommerce-smart-coupons' ),
/* translators: Documentation link for 'How to Apply Single or Multiple Coupons on Click of a Link' */
'ans' => esc_html__( 'URL of coupon should be like this:', 'woocommerce-smart-coupons' ) . ' <code>https://www.mysite.com/?coupon-code=discount5&sc-page=shop</code> ' . esc_html__( '. Replace www.mysite.com with your own site URL and replace discount5 with the your coupon code.', 'woocommerce-smart-coupons' ) . ' ' . sprintf( esc_html__( 'For more details you can refer to this article: %s', 'woocommerce-smart-coupons' ), '<a href="https://woocommerce.com/document/smart-coupons/how-to-apply-single-or-multiple-coupons-on-click-of-a-link/" target="_blank">' . esc_html__( 'How to Apply Single or Multiple Coupons on Click of a Link', 'woocommerce-smart-coupons' ) . '</a>' ),
),
array(
'que' => esc_html__( 'Do not want to tie store credit to be used by only one customer? / Can a customer send a gift certificate to themselves to pass on to someone else?', 'woocommerce-smart-coupons' ),
'ans' => esc_html__( 'Edit the main coupon which is entered in "Coupons" field of the product edit page, then go to "Usage Restrictions" > "Disable Email Restriction" and disable this setting and save the coupon.', 'woocommerce-smart-coupons' ),
),
array(
'que' => esc_html__( 'Getting \'Page Not Found Error\' when accessing Coupons tab from My Account Page?', 'woocommerce-smart-coupons' ),
'ans' => esc_html__( 'Go to WordPress -> Settings -> Permalinks and click on Save Settings once.', 'woocommerce-smart-coupons' ),
),
array(
'que' => esc_html__( 'Is there any reference file for creating an import file for coupons?', 'woocommerce-smart-coupons' ),
/* translators: 1. File name 2. File download link */
'ans' => sprintf( esc_html__( 'There is one file which is located inside the plugin. You can download the %1$s file from %2$s.', 'woocommerce-smart-coupons' ), '<code>sample.csv</code>', '<a href="' . esc_url( plugins_url( dirname( dirname( plugin_basename( __FILE__ ) ) ) . '/sample.csv' ) ) . '">' . esc_html__( 'here', 'woocommerce-smart-coupons' ) . '</a>' ) . ' ' . esc_html__( 'If you want to import coupon through file, the file should be like', 'woocommerce-smart-coupons' ) . ' <code>sample.csv</code>',
),
array(
'que' => esc_html__( 'Available coupons are not visible on Cart, Checkout & My Account page?', 'woocommerce-smart-coupons' ),
'ans' => esc_html__( 'Smart Coupons uses hooks of Cart, Checkout & My Account page to display available coupons. If your theme is not using those hooks in cart, checkout & my-account template, coupons will not be displayed.', 'woocommerce-smart-coupons' ),
),
array(
'que' => esc_html__( 'How can I resend gift card coupon bought by customers?', 'woocommerce-smart-coupons' ),
'ans' => esc_html__( 'You can resend them from order admin edit page.', 'woocommerce-smart-coupons' ) . ' <a href="https://woocommerce.com/document/smart-coupons/how-to-resend-coupons-generated-from-an-order-to-the-buyer/" target="_blank">' . __( 'See how', 'woocommerce-smart-coupons' ) . '</a>.',
),
array(
'que' => esc_html__( 'Uncheck "Auto-generate" option in Store Credit is not saving? Is it always checked?', 'woocommerce-smart-coupons' ),
'ans' => esc_html__( 'Store Credit\'s default behavior is auto-generate because, when using a store credit, it\'s balance keeps reducing. Therefore it should be uniquely created for every user automatically.', 'woocommerce-smart-coupons' ),
),
array(
'que' => esc_html__( 'Smart Coupons is not sending emails.', 'woocommerce-smart-coupons' ),
'ans' => esc_html__( 'Smart Coupons sends email only after order completion. So make sure that order complete email is enabled and sending. If enabled, then make sure all settings of coupons, products are in place. Also check by switching your theme.', 'woocommerce-smart-coupons' ),
),
array(
'que' => esc_html__( '"Store Credit Receiver detail" form not appearing on checkout page?', 'woocommerce-smart-coupons' ),
'ans' => esc_html__( 'This form is displayed using a hook which is available in My Account template. Make sure your theme\'s my-account template contains all hooks required for that template. Update your theme if it is not updated.', 'woocommerce-smart-coupons' ),
),
array(
'que' => esc_html__( 'Does Smart Coupons allow printing of coupon as Gift Card?', 'woocommerce-smart-coupons' ),
/* translators: Documentation link for 'How to Print Coupons' */
'ans' => sprintf( esc_html__( 'Yes, Smart Coupons does provide a feature for printing coupons. For more details, check this article: %s', 'woocommerce-smart-coupons' ), '<a href="https://woocommerce.com/document/smart-coupons/how-to-print-coupons/" target="_blank">' . esc_html__( 'How to Print Coupons', 'woocommerce-smart-coupons' ) . '</a>' ),
),
array(
'que' => esc_html__( 'Is it possible to have a coupon for each variation of the variable product?', 'woocommerce-smart-coupons' ),
'ans' => esc_html__( 'From version 4.11.0, you can add/link coupons to product variations as well. This feature is not available in a version lower than 4.11.0.', 'woocommerce-smart-coupons' ),
),
array(
'que' => esc_html__( 'Is Smart Coupons compatible with WooCommerce Subscriptions?', 'woocommerce-smart-coupons' ),
'ans' => esc_html__( 'Yes, Smart Coupons does work with WooCommerce Subscriptions.', 'woocommerce-smart-coupons' ),
),
array(
'que' => esc_html__( 'Which features of Smart Coupons work with Subscriptions?', 'woocommerce-smart-coupons' ),
'ans' => esc_html__( 'Give away a discount or credit on signing up a subscription, give away recurring discount or credits, apply credit during sign up, automatic payment for renewals from credit (Note: When using PayPal Standard Gateway, store credit can be applied only during sign up. Automatic payment for renewals by credit will not work for PayPal Standard Gateway).', 'woocommerce-smart-coupons' ),
),
array(
'que' => esc_html__( 'How does automatic payment by store credit work with Subscriptions?', 'woocommerce-smart-coupons' ),
'ans' => esc_html__( 'Customers can apply store credit on a subscription during purchase of subscription. If the same store credit has sufficient balance, it\'ll keep applying it to renewals till the remainder in store credit is higher than renewal price. Customers will be able to apply store credit only during signup. They will not get an option to apply store credit in renewals. But if the store credit will not have sufficient balance to pay for the renewals, then the order will go into pending mode. Now when the customer will go to pay for this renewal order, they\'ll get an option to apply store credit again. To activate the subscription again, the customer will have to pay for the renewals. When the customer is paying for the renewals from their account, then in that process they can use the same store credit which didn\'t have the sufficient balance, again & pay for the remaining amount.', 'woocommerce-smart-coupons' ),
),
array(
'que' => esc_html__( 'Is it possible to partially pay for a subscription with store credit and the remainder by another method?', 'woocommerce-smart-coupons' ),
'ans' => esc_html__( 'No, this is possible only in those cases where subscription amount is more than store credit\'s balance. If store credit\'s balance is more than subscription\'s total then your bank account or credit card will not be charged.', 'woocommerce-smart-coupons' ),
),
array(
'que' => esc_html__( 'Is Smart Coupons WPML compatible?', 'woocommerce-smart-coupons' ),
'ans' => esc_html__( 'Not yet, but this is being worked on. You will find this in later versions.', 'woocommerce-smart-coupons' ),
),
array(
'que' => esc_html__( 'I\'m using WPML & WPML provides support for multi-currency, but Smart Coupons only changes currency symbol & the price value remains same. Can Smart Coupons change the currency symbol and the price value associated with it?', 'woocommerce-smart-coupons' ),
/* translators: Link for the plugin 'Aelia Currency Switcher for WooCommerce' */
'ans' => sprintf( esc_html__( 'Currently, Smart Coupons is compatible with %s. But it is not compatible with any other multi-currency plugin or with WPML.', 'woocommerce-smart-coupons' ), '<a href="https://aelia.co/shop/currency-switcher-woocommerce/" target="_blank">' . esc_html__( 'Aelia Currency Switcher for WooCommerce', 'woocommerce-smart-coupons' ) . '</a>' ),
),
);
$faqs = array_chunk( $faqs, 2 );
$right_faq_numbering = 1;
$left_faq_numbering = 0;
echo '<div>';
foreach ( $faqs as $fqs ) {
echo '<div class="has-2-columns two-col">';
foreach ( $fqs as $index => $faq ) {
echo '<div' . ( ( 1 === absint( $index ) ) ? ' class="column col last-feature"' : ' class="column col"' ) . '>';
echo '<h4>' . ( ( 1 === absint( $index ) ) ? $right_faq_numbering : ( $left_faq_numbering + 1 ) ) . '. ' . $faq['que'] . '</h4>'; // phpcs:ignore
echo '<p>' . $faq['ans'] . '</p>'; // phpcs:ignore
echo '</div>';
$right_faq_numbering++;
$left_faq_numbering++;
}
echo '</div>';
}
echo '</div>';
?>
<div class="aligncenter">
<h3>
<?php
/* translators: WooCommerce My Account support link */
echo sprintf( __( 'If you are facing any issues, please %s from your WooCommerce account.', 'woocommerce-smart-coupons' ), '<a target="_blank" href="https://woocommerce.com/my-account/create-a-ticket/">' . esc_html__( 'submit a ticket', 'woocommerce-smart-coupons' ) . '</a>' ); // phpcs:ignore
?>
</h3>
</div>
</div>
<?php
}
/**
* Sends user to the welcome page on first activation.
*/
public function sc_welcome() {
if ( ! get_transient( '_smart_coupons_activation_redirect' ) ) {
return;
}
// Delete the redirect transient.
delete_transient( '_smart_coupons_activation_redirect' );
wp_safe_redirect( admin_url( 'admin.php?page=sc-about' ) );
exit;
}
}
}
WC_SC_Admin_Welcome::get_instance();

View File

@@ -0,0 +1,402 @@
<?php
/**
* Smart Coupons Ajax Actions
*
* @author StoreApps
* @since 3.3.0
* @version 1.7.0
*
* @package woocommerce-smart-coupons/includes/
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
if ( ! class_exists( 'WC_SC_Ajax' ) ) {
/**
* Class for handling ajax actions for Smart Coupons
*/
class WC_SC_Ajax {
/**
* Variable to hold instance of WC_SC_Ajax
*
* @var $instance
*/
private static $instance = null;
/**
* Constructor
*/
private function __construct() {
add_action( 'wp_ajax_sc_json_search_coupons', array( $this, 'sc_json_search_coupons' ) );
add_action( 'wp_ajax_sc_json_search_storewide_coupons', array( $this, 'sc_json_search_storewide_coupons' ) );
add_action( 'wp_ajax_smart_coupons_json_search', array( $this, 'smart_coupons_json_search' ) );
add_action( 'wp_ajax_hide_notice_delete_after_usage', array( $this, 'hide_notice_delete_after_usage' ) );
}
/**
* Get single instance of WC_SC_Ajax
*
* @return WC_SC_Ajax Singleton object of WC_SC_Ajax
*/
public static function get_instance() {
// Check if instance is already exists.
if ( is_null( self::$instance ) ) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Handle call to functions which is not available in this class
*
* @param string $function_name The function name.
* @param array $arguments Array of arguments passed while calling $function_name.
* @return result of function call
*/
public function __call( $function_name, $arguments = array() ) {
global $woocommerce_smart_coupon;
if ( ! is_callable( array( $woocommerce_smart_coupon, $function_name ) ) ) {
return;
}
if ( ! empty( $arguments ) ) {
return call_user_func_array( array( $woocommerce_smart_coupon, $function_name ), $arguments );
} else {
return call_user_func( array( $woocommerce_smart_coupon, $function_name ) );
}
}
/**
* Function to search coupons
*
* @param string $x Search term.
* @param array $post_types Post types.
*/
public function sc_json_search_coupons( $x = '', $post_types = array( 'shop_coupon' ) ) {
global $wpdb;
check_ajax_referer( 'search-coupons', 'security' );
$term = (string) wc_clean( wp_unslash( $_GET['term'] ) ); // phpcs:ignore
if ( empty( $term ) ) {
die();
}
$args = array(
'post_type' => $post_types,
'post_status' => 'publish',
'posts_per_page' => -1,
's' => $term,
'fields' => 'all',
);
$posts = wp_cache_get( 'wc_sc_search_coupon_by_code_' . sanitize_title( $term ), 'woocommerce_smart_coupons' );
if ( false === $posts ) {
$posts = $wpdb->get_results( // phpcs:ignore
$wpdb->prepare(
"SELECT * FROM {$wpdb->prefix}posts
WHERE post_type = %s
AND post_title LIKE %s
AND post_status = %s",
'shop_coupon',
$wpdb->esc_like( $term ) . '%',
'publish'
)
);
wp_cache_set( 'wc_sc_search_coupon_by_code_' . sanitize_title( $term ), $posts, 'woocommerce_smart_coupons' );
$this->maybe_add_cache_key( 'wc_sc_search_coupon_by_code_' . sanitize_title( $term ) );
}
$found_products = array();
$all_discount_types = wc_get_coupon_types();
if ( $posts && ! is_scalar( $posts ) ) {
foreach ( $posts as $post ) {
$discount_type = ( ! empty( $post->ID ) ) ? $this->get_post_meta( $post->ID, 'discount_type', true ) : 'fixed_cart';
if ( ! empty( $all_discount_types[ $discount_type ] ) ) {
$discount_type = ' (' . __( 'Type', 'woocommerce-smart-coupons' ) . ': ' . $all_discount_types[ $discount_type ] . ')';
$found_products[ $post->post_title ] = $post->post_title . $discount_type;
}
}
}
wp_send_json( $found_products );
}
/**
* Function to search storewide coupons
*
* @param string $x Search term.
* @param array $post_types Post types.
*/
public function sc_json_search_storewide_coupons( $x = '', $post_types = array( 'shop_coupon' ) ) {
global $wpdb;
check_ajax_referer( 'search-coupons', 'security' );
$term = (string) wc_clean( wp_unslash( $_GET['term'] ) ); // phpcs:ignore
if ( empty( $term ) ) {
die();
}
$found_coupons = array();
$coupon_posts = array();
$posts = wp_cache_get( 'wc_sc_search_storewide_coupon_by_code_' . sanitize_title( $term ), 'woocommerce_smart_coupons' );
if ( false === $posts ) {
$posts = $wpdb->get_results( // phpcs:ignore
$wpdb->prepare(
"SELECT p.ID,
p.post_title,
pm.meta_key,
pm.meta_value
FROM {$wpdb->posts} AS p
JOIN {$wpdb->postmeta} AS pm
ON (p.ID = pm.post_id AND pm.meta_key IN (%s,%s,%s,%s,%s,%s))
WHERE p.post_type = %s
AND p.post_title LIKE %s
AND p.post_status = %s",
'discount_type',
'coupon_amount',
'date_expires',
'auto_generate_coupon',
'customer_email',
'wc_sc_expiry_time',
'shop_coupon',
$wpdb->esc_like( $term ) . '%',
'publish'
),
ARRAY_A
);
wp_cache_set( 'wc_sc_search_storewide_coupon_by_code_' . sanitize_title( $term ), $posts, 'woocommerce_smart_coupons' );
$this->maybe_add_cache_key( 'wc_sc_search_storewide_coupon_by_code_' . sanitize_title( $term ) );
}
if ( ! empty( $posts ) ) {
foreach ( $posts as $post ) {
$post_id = ( ! empty( $post['ID'] ) ) ? absint( $post['ID'] ) : 0;
$post_title = ( ! empty( $post['post_title'] ) ) ? $post['post_title'] : '';
if ( empty( $post_id ) || empty( $post_title ) ) {
continue;
}
if ( empty( $coupon_posts[ $post_id ] ) || ! is_array( $coupon_posts[ $post_id ] ) ) {
$coupon_posts[ $post_id ] = array();
}
$coupon_posts[ $post_id ]['post_id'] = $post_id;
$coupon_posts[ $post_id ]['post_title'] = $post_title;
switch ( $post['meta_key'] ) {
case 'discount_type':
case 'coupon_amount':
case 'date_expires':
case 'auto_generate_coupon':
case 'wc_sc_expiry_time':
$coupon_posts[ $post_id ][ $post['meta_key'] ] = $post['meta_value']; // phpcs:ignore
break;
case 'customer_email':
$coupon_posts[ $post_id ][ $post['meta_key'] ] = maybe_unserialize( $post['meta_value'] ); // phpcs:ignore
break;
}
}
}
$all_discount_types = wc_get_coupon_types();
if ( ! empty( $coupon_posts ) ) {
foreach ( $coupon_posts as $post_id => $coupon_post ) {
$discount_type = ( ! empty( $coupon_post['discount_type'] ) ) ? $coupon_post['discount_type'] : '';
$coupon_amount = ( ! empty( $coupon_post['coupon_amount'] ) ) ? $coupon_post['coupon_amount'] : 0;
$date_expires = ( ! empty( $coupon_post['date_expires'] ) ) ? absint( $coupon_post['date_expires'] ) : 0;
$wc_sc_expiry_time = ( ! empty( $coupon_post['wc_sc_expiry_time'] ) ) ? absint( $coupon_post['wc_sc_expiry_time'] ) : 0;
$auto_generate_coupon = ( ! empty( $coupon_post['auto_generate_coupon'] ) ) ? $coupon_post['auto_generate_coupon'] : '';
$customer_email = ( ! empty( $coupon_post['customer_email'] ) ) ? $coupon_post['customer_email'] : array();
if ( empty( $discount_type ) || 'smart_coupon' === $discount_type ) {
continue;
}
if ( empty( $coupon_amount ) ) {
continue;
}
if ( ! empty( $date_expires ) ) {
$date_expires += $wc_sc_expiry_time;
if ( time() >= $date_expires ) {
continue;
}
}
if ( 'yes' === $auto_generate_coupon ) {
continue;
}
if ( ! empty( $customer_email ) ) {
continue;
}
if ( ! empty( $all_discount_types[ $discount_type ] ) ) {
/* translators: 1. The coupon code, 2. The discount type */
$found_coupons[ $coupon_post['post_title'] ] = sprintf( __( '%1$s (Type: %2$s)', 'woocommerce-smart-coupons' ), $coupon_post['post_title'], $all_discount_types[ $discount_type ] );
}
}
$found_coupons = apply_filters(
'wc_sc_json_search_storewide_coupons',
$found_coupons,
array(
'source' => $this,
'search_text' => $term,
'posts' => $posts,
'coupon_posts' => $coupon_posts,
)
);
}
wp_send_json( $found_coupons );
}
/**
* JSON Search coupon via ajax
*
* @param string $x Search text.
* @param array $post_types Post types.
*/
public function smart_coupons_json_search( $x = '', $post_types = array( 'shop_coupon' ) ) {
global $wpdb, $store_credit_label;
check_ajax_referer( 'search-coupons', 'security' );
$term = (string) wc_clean( wp_unslash( $_GET['term'] ) ); // phpcs:ignore
if ( empty( $term ) ) {
die();
}
$posts = wp_cache_get( 'wc_sc_shortcode_search_coupon_by_code_' . sanitize_title( $term ), 'woocommerce_smart_coupons' );
if ( false === $posts ) {
$posts = $wpdb->get_results( // phpcs:ignore
$wpdb->prepare(
"SELECT *
FROM {$wpdb->prefix}posts
WHERE post_type = %s
AND post_title LIKE %s
AND post_status = %s",
'shop_coupon',
$wpdb->esc_like( $term ) . '%',
'publish'
)
);
wp_cache_set( 'wc_sc_shortcode_search_coupon_by_code_' . sanitize_title( $term ), $posts, 'woocommerce_smart_coupons' );
$this->maybe_add_cache_key( 'wc_sc_shortcode_search_coupon_by_code_' . sanitize_title( $term ) );
}
$found_products = array();
$all_discount_types = wc_get_coupon_types();
if ( $posts ) {
foreach ( $posts as $post ) {
$discount_type = ( ! empty( $post->ID ) ) ? $this->get_post_meta( $post->ID, 'discount_type', true ) : 'fixed_cart';
if ( ! empty( $all_discount_types[ $discount_type ] ) ) {
$coupon = new WC_Coupon( $post->post_title );
if ( $this->is_wc_gte_30() ) {
$discount_type = $coupon->get_discount_type();
} else {
$discount_type = ( ! empty( $coupon->discount_type ) ) ? $coupon->discount_type : '';
}
$coupon_amount = $this->get_amount( $coupon );
switch ( $discount_type ) {
case 'smart_coupon':
$coupon_type = ! empty( $store_credit_label['singular'] ) ? ucwords( $store_credit_label['singular'] ) : __( 'Store Credit', 'woocommerce-smart-coupons' );
$coupon_amount = wc_price( $coupon_amount );
break;
case 'fixed_cart':
$coupon_type = __( 'Cart Discount', 'woocommerce-smart-coupons' );
$coupon_amount = wc_price( $coupon_amount );
break;
case 'fixed_product':
$coupon_type = __( 'Product Discount', 'woocommerce-smart-coupons' );
$coupon_amount = wc_price( $coupon_amount );
break;
case 'percent_product':
$coupon_type = __( 'Product Discount', 'woocommerce-smart-coupons' );
$coupon_amount = $coupon_amount . '%';
break;
case 'percent':
$coupon_type = ( $this->is_wc_gte_30() ) ? __( 'Discount', 'woocommerce-smart-coupons' ) : __( 'Cart Discount', 'woocommerce-smart-coupons' );
$coupon_amount = $coupon_amount . '%';
$max_discount = $this->get_post_meta( $post->ID, 'wc_sc_max_discount', true );
if ( ! empty( $max_discount ) && is_numeric( $max_discount ) ) {
/* translators: %s: Maximum coupon discount amount */
$coupon_type .= ' ' . sprintf( __( 'upto %s', 'woocommerce-smart-coupons' ), wc_price( $max_discount ) );
}
break;
default:
$default_coupon_type = ( ! empty( $all_discount_types[ $discount_type ] ) ) ? $all_discount_types[ $discount_type ] : ucwords( str_replace( array( '_', '-' ), ' ', $discount_type ) );
$coupon_type = apply_filters( 'wc_sc_coupon_type', $default_coupon_type, $coupon, $all_discount_types );
$coupon_amount = apply_filters( 'wc_sc_coupon_amount', $coupon_amount, $coupon );
break;
}
$discount_type = ' ( ' . $coupon_amount . ' ' . $coupon_type . ' )';
$discount_type = wp_strip_all_tags( $discount_type );
$found_products[ $post->post_title ] = $post->post_title . ' ' . $discount_type;
}
}
}
if ( ! empty( $found_products ) ) {
echo wp_json_encode( $found_products );
}
die();
}
/**
* Function to Hide Notice Delete After Usage
*/
public function hide_notice_delete_after_usage() {
check_ajax_referer( 'hide-smart-coupons-notice', 'security' );
$current_user_id = get_current_user_id();
update_user_meta( $current_user_id, 'hide_delete_credit_after_usage_notice', 'yes' );
wp_send_json( array( 'message' => 'success' ) );
}
}
}
WC_SC_Ajax::get_instance();

View File

@@ -0,0 +1,633 @@
<?php
/**
* Processing of coupons
*
* @author StoreApps
* @since 3.3.0
* @version 2.1.0
* @package WooCommerce Smart Coupons
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
if ( ! class_exists( 'WC_SC_Apply_Before_Tax' ) ) {
/**
* Class for applying store credit before tax calculation
*/
class WC_SC_Apply_Before_Tax {
/**
* Variable to hold instance of WC_SC_Apply_Before_Tax
*
* @var $instance
*/
private static $instance = null;
/**
* Store credit left after application on each cart item
*
* @var $sc_credit_left
*/
private $sc_credit_left = array();
/**
* Store credit left after application on each cart item in REST API
*
* @var $sc_api_credit_left
*/
private $sc_api_credit_left = array();
/**
* Remaining total to apply credit
*
* @var $remaining_total_to_apply_credit
*/
private $remaining_total_to_apply_credit = array();
/**
* Constructor
*/
private function __construct() {
add_action( 'woocommerce_order_before_calculate_totals', array( $this, 'order_calculate_discount_amount_before_tax' ), 10, 2 );
add_action( 'woocommerce_checkout_create_order', array( $this, 'cart_set_discount_total' ), 10, 1 );
add_action( 'woocommerce_before_calculate_totals', array( $this, 'cart_calculate_discount_amount' ), 20 );
add_filter( 'woocommerce_coupon_get_discount_amount', array( $this, 'cart_return_discount_amount' ), 20, 5 );
add_filter( 'woocommerce_coupon_custom_discounts_array', array( $this, 'store_credit_discounts_array' ), 10, 2 );
add_filter( 'woocommerce_add_cart_item', array( $this, 'sc_mnm_compat' ), 20, 2 );
add_action( 'woocommerce_after_calculate_totals', array( $this, 'cart_set_total_credit_used' ) );
add_action( 'woocommerce_before_calculate_totals', array( $this, 'cart_reset_credit_left' ), 15 );
add_filter( 'woocommerce_coupon_sort', array( $this, 'change_default_sort_order' ), 20, 2 );
}
/**
* Get single instance of WC_SC_Apply_Before_Tax
*
* @return WC_SC_Apply_Before_Tax Singleton object of WC_SC_Apply_Before_Tax
*/
public static function get_instance() {
// Check if instance is already exists.
if ( is_null( self::$instance ) ) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Handle call to functions which is not available in this class
*
* @param string $function_name Function to call.
* @param array $arguments Array of arguments passed while calling $function_name.
* @return mixed Result of function call.
*/
public function __call( $function_name, $arguments = array() ) {
global $woocommerce_smart_coupon;
if ( ! is_callable( array( $woocommerce_smart_coupon, $function_name ) ) ) {
return;
}
if ( ! empty( $arguments ) ) {
return call_user_func_array( array( $woocommerce_smart_coupon, $function_name ), $arguments );
} else {
return call_user_func( array( $woocommerce_smart_coupon, $function_name ) );
}
}
/**
* Function to apply store credit before tax calculation for orders that are manually created and updated from backend
*
* @param bool $and_taxes Calc taxes if true.
* @param WC_Order $order Order object.
*/
public function order_calculate_discount_amount_before_tax( $and_taxes, $order ) {
$order_actions = array( 'woocommerce_add_coupon_discount', 'woocommerce_calc_line_taxes', 'woocommerce_save_order_items' );
$order_id = ( $this->is_callable( $order, 'get_id' ) ) ? $order->get_id() : 0;
$post_post_type = ( ! empty( $_POST['post_type'] ) ) ? wc_clean( wp_unslash( $_POST['post_type'] ) ) : ''; // phpcs:ignore
$post_action = ( ! empty( $_POST['action'] ) ) ? wc_clean( wp_unslash( $_POST['action'] ) ) : ''; // phpcs:ignore
$post_type = ( ! empty( $order_id ) && $this->is_hpos() ) ? $this->get_post_type( $order_id ) : $post_post_type;
if ( $order instanceof WC_Order && ( in_array( $post_action, $order_actions, true ) || ( 'shop_order' === $post_type && in_array( $post_action, array( 'edit_order', 'editpost' ), true ) ) ) ) {
if ( ! is_object( $order ) || ! is_callable( array( $order, 'get_id' ) ) ) {
return;
}
if ( empty( $order_id ) ) {
return;
}
$coupons = $order->get_items( 'coupon' );
$order_items = $order->get_items( 'line_item' );
if ( empty( $order_items ) && empty( $coupons ) ) {
return;
}
foreach ( $coupons as $item_id => $item ) {
$coupon_code = ( is_object( $item ) && is_callable( array( $item, 'get_name' ) ) ) ? $item->get_name() : $item['name'];
if ( empty( $coupon_code ) ) {
continue;
}
$coupon = new WC_Coupon( $coupon_code );
$discount_type = $coupon->get_discount_type();
if ( 'smart_coupon' === $discount_type ) {
$sc_include_tax = $this->is_store_credit_include_tax();
$smart_coupons_contribution = $this->get_post_meta( $order_id, 'smart_coupons_contribution', true, true );
$smart_coupons_contribution = ( ! empty( $smart_coupons_contribution ) ) ? $smart_coupons_contribution : array();
$discount_amount = ( is_object( $item ) && is_callable( array( $item, 'get_discount' ) ) ) ? $item->get_discount() : $this->get_order_item_meta( $item_id, 'discount_amount', true, true );
$discount_amount_tax = ( is_object( $item ) && is_callable( array( $item, 'get_discount_tax' ) ) ) ? $item->get_discount_tax() : $this->get_order_item_meta( $item_id, 'discount_amount_tax', true, true );
if ( is_array( $smart_coupons_contribution ) && count( $smart_coupons_contribution ) > 0 && array_key_exists( $coupon_code, $smart_coupons_contribution ) ) {
// If store credit discount is inclusive of tax then remove discount given tax from Smart Coupons' contribution.
if ( 'yes' === $sc_include_tax && ! empty( $discount_amount_tax ) ) {
$new_discount = $smart_coupons_contribution[ $coupon_code ] - $discount_amount_tax;
} else {
$new_discount = $smart_coupons_contribution[ $coupon_code ];
}
if ( is_object( $item ) && is_callable( array( $item, 'set_discount' ) ) ) {
$item->set_discount( $new_discount );
} else {
$item['discount_amount'] = $new_discount;
}
} elseif ( ! empty( $discount_amount ) ) {
if ( is_object( $item ) && is_callable( array( $item, 'set_discount' ) ) ) {
$item->set_discount( $discount_amount );
} else {
$item['discount_amount'] = $discount_amount;
}
// If discount includes tax then Smart Coupons contribution is sum of discount on product price and discount on tax.
if ( 'yes' === $sc_include_tax && ! empty( $discount_amount_tax ) ) {
$smart_coupons_contribution[ $coupon_code ] = $discount_amount + $discount_amount_tax;
} else {
$smart_coupons_contribution[ $coupon_code ] = $discount_amount;
}
$this->update_post_meta( $order_id, 'smart_coupons_contribution', $smart_coupons_contribution, true );
} else {
$coupon_amount = $this->get_amount( $coupon, true, $order );
$coupon_product_ids = $coupon->get_product_ids();
$coupon_category_ids = $coupon->get_product_categories();
$subtotal = 0;
$items_to_apply_credit = array();
if ( count( $coupon_product_ids ) > 0 || count( $coupon_category_ids ) > 0 ) {
foreach ( $order_items as $order_item_id => $order_item ) {
$product_category_ids = wc_get_product_cat_ids( $order_item['product_id'] );
if ( count( $coupon_product_ids ) > 0 && count( $coupon_category_ids ) > 0 ) {
if ( ( in_array( $order_item['product_id'], $coupon_product_ids, true ) || in_array( $order_item['variation_id'], $coupon_product_ids, true ) ) && count( array_intersect( $product_category_ids, $coupon_category_ids ) ) > 0 ) {
$items_to_apply_credit[] = $order_item_id;
}
} else {
if ( in_array( $order_item['product_id'], $coupon_product_ids, true ) || in_array( $order_item['variation_id'], $coupon_product_ids, true ) || count( array_intersect( $product_category_ids, $coupon_category_ids ) ) > 0 ) {
$items_to_apply_credit[] = $order_item_id;
}
}
}
} else {
$items_to_apply_credit = array_keys( $order_items );
}
$subtotal = array_sum( array_map( array( $this, 'sc_get_order_subtotal' ), $items_to_apply_credit ) );
if ( $subtotal <= 0 ) {
continue;
}
$store_credit_used = 0;
foreach ( $items_to_apply_credit as $order_item_id ) {
$order_item = $order_items[ $order_item_id ];
$discounting_amount = $order_item->get_total();
// If discount include tax then add item tax to discounting amount to allow discount calculation on tax also.
if ( 'yes' === $sc_include_tax ) {
$item_tax = ( is_callable( array( $order, 'get_line_tax' ) ) ) ? $order->get_line_tax( $order_item ) : 0;
$discounting_amount += $item_tax;
}
$quantity = $order_item->get_quantity();
$discount = $this->sc_get_discounted_price( $discounting_amount, $quantity, $subtotal, $coupon_amount );
$discount *= $quantity;
$order_item->set_total( $discounting_amount - $discount );
$store_credit_used += $discount;
}
if ( is_object( $item ) && is_callable( array( $item, 'set_discount' ) ) ) {
$item->set_discount( $store_credit_used );
} else {
$item['discount_amount'] = $store_credit_used;
}
$smart_coupons_contribution[ $coupon_code ] = $store_credit_used;
$this->update_post_meta( $order_id, 'smart_coupons_contribution', $smart_coupons_contribution, true );
}
$order->sc_total_credit_used = $smart_coupons_contribution;
}
}
}
}
/**
* Function to calculate subtotal of items in order which is necessary for applying store credit before tax calculation
*
* @param int $order_item_id Item ID.
* @return float $subtotal
*/
private function sc_get_order_subtotal( $order_item_id ) {
$order_item = WC_Order_Factory::get_order_item( $order_item_id );
$subtotal = $order_item->get_total();
$prices_include_tax = wc_prices_include_tax();
// Get global setting for whether store credit discount is inclusive of tax or not.
$sc_include_tax = get_option( 'woocommerce_smart_coupon_include_tax', 'no' );
// If prices are inclusive of tax and discount amount is also inclusive of tax then add item tax in subtotal to handle discount calculation correctly.
if ( true === $prices_include_tax && 'yes' === $sc_include_tax ) {
$subtotal += $order_item->get_total_tax();
}
return $subtotal;
}
/**
* Function to update_discount_total for an order
*
* @param WC_Order $order Order object.
* @param float $total_credit_used Total store credit used.
*/
public function update_discount_total( $order = '', $total_credit_used = 0 ) {
if ( $order instanceof WC_Order ) {
$discount_total = $order->get_discount_total();
$sc_credit_used = min( $discount_total, $total_credit_used );
$order->set_discount_total( $discount_total - $sc_credit_used );
}
}
/**
* Function to set discount total for a new order
*
* @param WC_Order $order Order object.
*/
public function cart_set_discount_total( $order ) {
if ( isset( WC()->cart->smart_coupon_credit_used ) && is_array( WC()->cart->smart_coupon_credit_used ) && count( WC()->cart->smart_coupon_credit_used ) > 0 ) {
$total_credit_used = array_sum( WC()->cart->smart_coupon_credit_used );
$this->update_discount_total( $order, $total_credit_used );
}
}
/**
* Function to apply store credit before tax calculation for cart items
*/
public function cart_calculate_discount_amount() {
$cart = ( isset( WC()->cart ) ) ? WC()->cart : '';
if ( $cart instanceof WC_Cart ) {
$cart_contents = WC()->cart->get_cart();
$coupons = $cart->get_coupons();
if ( ! empty( $coupons ) ) {
$items_to_apply_credit = array();
foreach ( $coupons as $coupon_code => $coupon ) {
$discount_type = $coupon->get_discount_type();
if ( 'smart_coupon' === $discount_type ) {
$coupon_product_ids = $coupon->get_product_ids();
$coupon_category_ids = $coupon->get_product_categories();
if ( count( $coupon_product_ids ) > 0 || count( $coupon_category_ids ) > 0 ) {
foreach ( $cart_contents as $cart_item_key => $cart_item ) {
$product_category_ids = wc_get_product_cat_ids( $cart_item['product_id'] );
if ( count( $coupon_product_ids ) > 0 && count( $coupon_category_ids ) > 0 ) {
if ( ( in_array( $cart_item['product_id'], $coupon_product_ids, true ) || in_array( $cart_item['variation_id'], $coupon_product_ids, true ) ) && count( array_intersect( $product_category_ids, $coupon_category_ids ) ) > 0 ) {
$items_to_apply_credit[ $coupon_code ][] = $cart_item_key;
}
} else {
if ( in_array( $cart_item['product_id'], $coupon_product_ids, true ) || in_array( $cart_item['variation_id'], $coupon_product_ids, true ) || count( array_intersect( $product_category_ids, $coupon_category_ids ) ) > 0 ) {
$items_to_apply_credit[ $coupon_code ][] = $cart_item_key;
}
}
}
} else {
$items_to_apply_credit[ $coupon_code ] = array_keys( $cart_contents );
}
}
}
if ( ! empty( $items_to_apply_credit ) ) {
WC()->cart->sc_items_to_apply_credit = $items_to_apply_credit;
}
}
}
}
/**
* Get discount amount for a cart item.
*
* @param float $discount Amount this coupon has discounted.
* @param float $discounting_amount Amount the coupon is being applied to.
* @param array|null $cart_item Cart item being discounted if applicable.
* @param bool $single True if discounting a single qty item, false if its the line.
* @param WC_Coupon $coupon Coupon object.
* @return float $discount
*/
public function cart_return_discount_amount( $discount, $discounting_amount, $cart_item, $single, $coupon ) {
if ( ! is_object( $coupon ) || ! is_a( $coupon, 'WC_Coupon' ) ) {
return $discount;
}
$discount_type = is_callable( array( $coupon, 'get_discount_type' ) ) ? $coupon->get_discount_type() : '';
if ( 'smart_coupon' !== $discount_type ) {
return $discount;
}
$coupon_code = is_callable( array( $coupon, 'get_code' ) ) ? $coupon->get_code() : '';
if ( is_object( $cart_item ) && is_a( $cart_item, 'WC_Order_Item_Product' ) ) {
return $this->calculate_discount_amount_for_rest_api( $discount, $discounting_amount, $cart_item, $single, $coupon );
}
$coupon_amount = $this->get_amount( $coupon, true );
$product = isset( $cart_item['data'] ) ? $cart_item['data'] : array();
$quantity = $cart_item['quantity'];
// Compatibility for WC version < 3.2.0.
if ( ! isset( $cart_item['key'] ) ) {
$product_id = ( ! empty( $cart_item['variation_id'] ) ) ? $cart_item['variation_id'] : $cart_item['product_id'];
foreach ( WC()->cart->cart_contents as $key => $cart_data ) {
$cart_data_product_id = ( ! empty( $cart_data['variation_id'] ) ) ? $cart_data['variation_id'] : $cart_data['product_id'];
if ( $product_id === $cart_data_product_id ) {
$cart_item['key'] = $key;
}
}
}
$prices_include_tax = ( 'incl' === get_option( 'woocommerce_tax_display_cart' ) ) ? true : false;
if ( true === $prices_include_tax ) {
$sc_include_tax = get_option( 'woocommerce_smart_coupon_include_tax', 'no' );
if ( 'no' === $sc_include_tax ) {
$discounting_amount = $cart_item['line_subtotal'] / $quantity;
}
}
$items_to_apply_credit = isset( WC()->cart->sc_items_to_apply_credit ) ? WC()->cart->sc_items_to_apply_credit : array();
if ( ! empty( $items_to_apply_credit ) && is_array( $items_to_apply_credit ) && array_key_exists( $coupon_code, $items_to_apply_credit ) && in_array( $cart_item['key'], $items_to_apply_credit[ $coupon_code ], true ) ) {
$credit_left = isset( $this->sc_credit_left[ $coupon_code ] ) ? $this->sc_credit_left[ $coupon_code ] : $coupon_amount;
$total_discounting_amount = $discounting_amount * $quantity;
if ( isset( $this->remaining_total_to_apply_credit[ $cart_item['key'] ] ) ) {
$total_discounting_amount = wc_remove_number_precision( $this->remaining_total_to_apply_credit[ $cart_item['key'] ] );
}
$applied_discount = min( $total_discounting_amount, $credit_left );
$this->sc_credit_left[ $coupon_code ] = ( $total_discounting_amount < $credit_left ) ? $credit_left - $total_discounting_amount : 0;
$discount = $applied_discount / $quantity;
}
return $discount;
}
/**
* Calculate discount amount for REST API and Order created via backend.
*
* @param float $discount Amount this coupon has discounted.
* @param float $discounting_amount Amount the coupon is being applied to.
* @param WC_Order_Item_Product $cart_item Object.
* @param bool $single True if discounting a single qty item, false if its the line.
* @param WC_Coupon $coupon Object.
* @return float|int|mixed
*/
public function calculate_discount_amount_for_rest_api( $discount = 0, $discounting_amount = 0, $cart_item = object, $single = false, $coupon = object ) {
if ( ! is_object( $coupon ) || ! is_a( $coupon, 'WC_Coupon' ) ) {
return $discount;
}
if ( ! is_object( $cart_item ) || ! is_a( $cart_item, 'WC_Order_Item_Product' ) ) {
return $discount;
}
$quantity = ( is_callable( array( $cart_item, 'get_quantity' ) ) ) ? $cart_item->get_quantity() : 1;
$item_id = ( is_callable( array( $cart_item, 'get_id' ) ) ) ? $cart_item->get_id() : 0;
$product_subtotal = ( is_callable( array( $cart_item, 'get_subtotal' ) ) ) ? $cart_item->get_subtotal() : 0;
$order = ( is_callable( array( $cart_item, 'get_order' ) ) ) ? $cart_item->get_order() : null;
$coupon_code = ( is_callable( array( $coupon, 'get_code' ) ) ) ? $coupon->get_code() : '';
$discount_type = ( is_callable( array( $coupon, 'get_discount_type' ) ) ) ? $coupon->get_discount_type() : '';
if ( 'smart_coupon' !== $discount_type ) {
return $discount;
}
$smart_coupons_contribution = ( is_callable( array( $order, 'get_meta' ) ) ) ? $order->get_meta( 'smart_coupons_contribution' ) : array();
if ( isset( $this->sc_api_credit_left[ $coupon_code ]['items'] ) && ! empty( $this->sc_api_credit_left[ $coupon_code ]['items'][ $item_id ] ) ) {
return $this->sc_api_credit_left[ $coupon_code ]['items'][ $item_id ];
}
$coupon_amount = is_array( $smart_coupons_contribution ) && isset( $smart_coupons_contribution[ $coupon_code ] ) ? $smart_coupons_contribution[ $coupon_code ] : (float) $this->get_amount( $coupon, true, $order );
// isset is required here to confirm that store credit was used but now it is empty.
if ( $coupon_amount < 1 || ( isset( $this->sc_api_credit_left[ $coupon_code ]['credit_left'] ) && empty( $this->sc_api_credit_left[ $coupon_code ]['credit_left'] ) ) ) {
return $discount;
}
$prices_include_tax = ( 'yes' === get_option( 'woocommerce_prices_include_tax' ) ) ? true : false;
if ( true === $prices_include_tax ) {
$sc_include_tax = get_option( 'woocommerce_smart_coupon_include_tax', 'no' );
if ( 'no' === $sc_include_tax ) {
$discounting_amount = $product_subtotal / $quantity;
}
}
$credit_left = ( ! empty( $this->sc_api_credit_left ) && isset( $this->sc_api_credit_left[ $coupon_code ]['credit_left'] ) ) ? $this->sc_api_credit_left[ $coupon_code ]['credit_left'] : $coupon_amount;
if ( $credit_left > 0 ) {
$total_credit_already_applied_current_item = is_array( $this->sc_api_credit_left ) && count( $this->sc_api_credit_left ) > 0 ? (float) array_sum( wp_list_pluck( array_column( array_values( $this->sc_api_credit_left ), 'items' ), $item_id ) ) : 0;
if ( $single ) {
$discount = min( $quantity * ( $discounting_amount - $total_credit_already_applied_current_item ), $credit_left );
} else {
$discount = min( ( $discounting_amount - ( $total_credit_already_applied_current_item * $quantity ) ), $credit_left );
}
$credit_left -= $discount;
$this->sc_api_credit_left[ $coupon_code ]['credit_left'] = $credit_left;
$discount = (float) $discount / $quantity;
}
// Note*: We always store discount credit inside sc_api_credit_left as discounting_amount not line total.
$this->sc_api_credit_left[ $coupon_code ]['items'][ $item_id ] = $discount;
return $single ? $discount : $discount * $quantity;
}
/**
* Discount details for store credit
*
* @param array $discounts The discount details.
* @param WC_Coupon $coupon The coupon object.
* @return array
*/
public function store_credit_discounts_array( $discounts = array(), $coupon = null ) {
$cart = ( isset( WC()->cart ) ) ? WC()->cart : '';
if ( $cart instanceof WC_Cart ) {
$cart_contents = ( is_object( WC()->cart ) && is_callable( array( WC()->cart, 'get_cart' ) ) ) ? WC()->cart->get_cart() : array();
if ( ! empty( $cart_contents ) ) {
$discount_type = ( is_object( $coupon ) && is_callable( array( $coupon, 'get_discount_type' ) ) ) ? $coupon->get_discount_type() : '';
if ( 'smart_coupon' === $discount_type ) {
$prices_include_tax = ( 'incl' === get_option( 'woocommerce_tax_display_cart' ) ) ? true : false;
if ( true === $prices_include_tax ) {
$sc_include_tax = get_option( 'woocommerce_smart_coupon_include_tax', 'no' );
if ( 'no' === $sc_include_tax ) {
if ( ! empty( $discounts ) ) {
foreach ( $discounts as $item_key => $discount ) {
$line_subtotal = wc_round_discount( wc_add_number_precision( $cart_contents[ $item_key ]['line_subtotal'] ), 0 );
$line_subtotal = ( isset( $this->remaining_total_to_apply_credit[ $item_key ] ) ) ? min( $this->remaining_total_to_apply_credit[ $item_key ], $line_subtotal ) : $line_subtotal;
$discount = min( $discount, $line_subtotal );
$discounts [ $item_key ] = $discount;
$this->remaining_total_to_apply_credit[ $item_key ] = $line_subtotal - $discount;
}
}
}
}
}
}
}
return $discounts;
}
/**
* Set smart coupon credit used
*/
public function cart_set_total_credit_used() {
$coupon_discount_totals = ( is_callable( array( 'WC_Cart', 'get_coupon_discount_totals' ) ) ) ? WC()->cart->get_coupon_discount_totals() : WC()->cart->coupon_discount_amounts;
$coupon_discount_tax_totals = ( is_callable( array( 'WC_Cart', 'get_coupon_discount_tax_totals' ) ) ) ? WC()->cart->get_coupon_discount_tax_totals() : WC()->cart->coupon_discount_tax_amounts;
$sc_total_credit_used = array();
if ( ! empty( $coupon_discount_totals ) && is_array( $coupon_discount_totals ) && count( $coupon_discount_totals ) > 0 ) {
foreach ( $coupon_discount_totals as $coupon_code => $total ) {
$coupon = new WC_Coupon( $coupon_code );
$discount_type = $coupon->get_discount_type();
if ( 'smart_coupon' === $discount_type ) {
$sc_total_credit_used[ $coupon_code ] = $total;
if ( ! empty( $coupon_discount_tax_totals[ $coupon_code ] ) ) {
$sc_include_tax = $this->is_store_credit_include_tax();
if ( 'yes' === $sc_include_tax ) {
$sc_total_credit_used[ $coupon_code ] += $coupon_discount_tax_totals[ $coupon_code ];
} else {
$prices_include_tax = ( 'incl' === get_option( 'woocommerce_tax_display_cart' ) ) ? true : false;
if ( true === $prices_include_tax ) {
$apply_before_tax = get_option( 'woocommerce_smart_coupon_apply_before_tax', 'no' );
if ( 'yes' === $apply_before_tax ) {
$_sc_include_tax = get_option( 'woocommerce_smart_coupon_include_tax', 'no' );
if ( 'no' === $_sc_include_tax ) {
$sc_total_credit_used[ $coupon_code ] += $coupon_discount_tax_totals[ $coupon_code ];
}
}
}
}
}
}
}
}
if ( ! empty( $sc_total_credit_used ) ) {
WC()->cart->smart_coupon_credit_used = $sc_total_credit_used;
}
}
/**
* Function to calculate discount amount for an item
*
* @param float $discounting_amount Amount the coupon is being applied to.
* @param int $quantity Item quantity.
* @param float $subtotal Cart/Order subtotal.
* @param float $coupon_amount Coupon amount.
* @return float $discount
*/
public function sc_get_discounted_price( $discounting_amount = 0, $quantity = 1, $subtotal = 0, $coupon_amount = 0 ) {
$discount = 0;
$discounting_amount = $discounting_amount / $quantity;
$discount_percent = ( $discounting_amount * $quantity ) / $subtotal;
$discount = ( $coupon_amount * $discount_percent ) / $quantity;
$discount = min( $discount, $discounting_amount );
return $discount;
}
/**
* Function to add cart item key for MNM child items.
* This was need because MNM child items didn't had cart item key inside $cart_item_data array and the
* function WC_SC_Apply_Before_Tax::cart_return_discount_amount() uses cart item key to set discount amount.
*
* @param array $cart_item_data Cart item data.
* @param string $cart_item_key Cart item key.
* @return float $cart_item_data
*/
public function sc_mnm_compat( $cart_item_data, $cart_item_key ) {
if ( ! empty( $cart_item_data['mnm_container'] ) ) {
$cart_item_data['key'] = $cart_item_key;
}
return $cart_item_data;
}
/**
* Reset credit left to the defaults.
*/
public function cart_reset_credit_left() {
$this->sc_credit_left = array();
$this->sc_api_credit_left = array();
$this->remaining_total_to_apply_credit = array();
}
/**
* Function to override smart coupon sequence order defauly by woocommerce.
*
* @param number $sort order sequence number.
* @param object $coupon object of Coupon.
* @return number $sort
*/
public function change_default_sort_order( $sort = 0, $coupon = null ) {
if ( is_admin() || ! $coupon instanceof WC_Coupon || is_null( $coupon ) || ! is_callable( array( $coupon, 'get_discount_type' ) ) || ( is_callable( array( $coupon, 'get_discount_type' ) ) && $coupon->get_discount_type() !== 'smart_coupon' ) ) {
return $sort;
}
// Allow plugins to override the default order.
return apply_filters( 'wc_sc_coupon_sort_order', 4, $coupon );
}
}
}
WC_SC_Apply_Before_Tax::get_instance();

View File

@@ -0,0 +1,751 @@
<?php
/**
* Auto apply coupon
*
* @author StoreApps
* @since 4.6.0
* @version 2.8.0
*
* @package woocommerce-smart-coupons/includes/
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
if ( ! class_exists( 'WC_SC_Auto_Apply_Coupon' ) ) {
/**
* Class for handling coupons applied via URL
*/
class WC_SC_Auto_Apply_Coupon {
/**
* Variable to hold instance of WC_SC_Auto_Apply_Coupon
*
* @var $instance
*/
private static $instance = null;
/**
* Variable to hold coupon notices
*
* @var $coupon_notices
*/
private $coupon_notices = array();
/**
* Constructor
*/
private function __construct() {
add_action( 'woocommerce_coupon_options', array( $this, 'usage_restriction' ), 10, 2 );
add_action( 'woocommerce_coupon_options_save', array( $this, 'process_meta' ), 10, 2 );
add_filter( 'wc_smart_coupons_export_headers', array( $this, 'export_headers' ) );
add_filter( 'smart_coupons_parser_postmeta_defaults', array( $this, 'postmeta_defaults' ) );
add_filter( 'sc_generate_coupon_meta', array( $this, 'generate_coupon_meta' ), 10, 2 );
add_filter( 'wc_sc_process_coupon_meta_value_for_import', array( $this, 'process_coupon_meta_value_for_import' ), 10, 2 );
add_filter( 'is_protected_meta', array( $this, 'make_action_meta_protected' ), 10, 3 );
add_action( 'wp_loaded', array( $this, 'handle_auto_apply_hooks' ), 15 );
add_action( 'woocommerce_cart_calculate_fees', array( $this, 'remove_coupon_if_zero' ), 10, 1 );
}
/**
* Get single instance of WC_SC_Auto_Apply_Coupon
*
* @return WC_SC_Auto_Apply_Coupon Singleton object of WC_SC_Auto_Apply_Coupon
*/
public static function get_instance() {
// Check if instance is already exists.
if ( is_null( self::$instance ) ) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Handle call to functions which is not available in this class
*
* @param string $function_name The function name.
* @param array $arguments Array of arguments passed while calling $function_name.
* @return result of function call
*/
public function __call( $function_name, $arguments = array() ) {
global $woocommerce_smart_coupon;
if ( ! is_callable( array( $woocommerce_smart_coupon, $function_name ) ) ) {
return;
}
if ( ! empty( $arguments ) ) {
return call_user_func_array( array( $woocommerce_smart_coupon, $function_name ), $arguments );
} else {
return call_user_func( array( $woocommerce_smart_coupon, $function_name ) );
}
}
/**
* Display field for auto apply coupon
*
* @param integer $coupon_id The coupon id.
* @param WC_Coupon $coupon The coupon object.
*/
public function usage_restriction( $coupon_id = 0, $coupon = null ) {
?>
<script type="text/javascript">
document.addEventListener('DOMContentLoaded', () => {
const showHideAutoApplyCouponField = () => {
let discountType = document.querySelector('select#discount_type').value;
let autoApplyField = document.querySelector('.wc_sc_auto_apply_coupon_field');
autoApplyField.style.display = ( 'smart_coupon' === discountType ) ? 'none' : 'block';
};
showHideAutoApplyCouponField();
document.querySelector('select#discount_type').addEventListener('change', () => {
showHideAutoApplyCouponField();
});
});
</script>
<div class="options_group smart-coupons-field">
<?php
$is_allow_auto_apply = $this->sc_get_option( 'wc_sc_allow_auto_apply', 'yes' );
if ( 'yes' === $is_allow_auto_apply ) {
woocommerce_wp_checkbox(
array(
'id' => 'wc_sc_auto_apply_coupon',
'label' => __( 'Auto apply?', 'woocommerce-smart-coupons' ),
'description' => __( 'When checked, this coupon will be applied automatically, if it is valid. If enabled in more than 5 coupons, only 5 coupons will be applied automatically, rest will be ignored.', 'woocommerce-smart-coupons' ),
)
);
}
?>
</div>
<?php
}
/**
* Save auto apply coupon in meta
*
* @param Integer $post_id The coupon post ID.
* @param WC_Coupon $coupon The coupon object.
*/
public function process_meta( $post_id = 0, $coupon = null ) {
if ( empty( $post_id ) ) {
return;
}
$coupon = new WC_Coupon( $coupon );
$is_callable_coupon_update_meta = $this->is_callable( $coupon, 'update_meta_data' ) && $this->is_callable( $coupon, 'save' );
// Get list of ids of coupons to auto apply.
$auto_apply_coupon_ids = get_option( 'wc_sc_auto_apply_coupon_ids', array() );
$auto_apply_coupon_ids = ( empty( $auto_apply_coupon_ids ) || ! is_array( $auto_apply_coupon_ids ) ) ? array() : $auto_apply_coupon_ids;
$auto_apply_coupon_ids = array_map( 'absint', $auto_apply_coupon_ids );
$post_id = absint( $post_id );
if ( isset( $_POST['wc_sc_auto_apply_coupon'] ) && isset( $_POST['discount_type'] ) && 'smart_coupon' !== wc_clean( wp_unslash( $_POST['discount_type'] ) ) ) { // phpcs:ignore
$auto_apply_coupon = wc_clean( wp_unslash( $_POST['wc_sc_auto_apply_coupon'] ) ); // phpcs:ignore
if ( true === $is_callable_coupon_update_meta ) {
$coupon->update_meta_data( 'wc_sc_auto_apply_coupon', $auto_apply_coupon );
$coupon->save();
} else {
$this->update_post_meta( $post_id, 'wc_sc_auto_apply_coupon', $auto_apply_coupon );
}
// Add coupon id to auto apply coupon list if haven't added already.
if ( is_array( $auto_apply_coupon_ids ) && ! in_array( $post_id, $auto_apply_coupon_ids, true ) ) {
$auto_apply_coupon_ids[] = $post_id;
}
} else {
if ( true === $is_callable_coupon_update_meta ) {
$coupon->update_meta_data( 'wc_sc_auto_apply_coupon', 'no' );
$coupon->save();
} else {
$this->update_post_meta( $post_id, 'wc_sc_auto_apply_coupon', 'no' );
}
// Remove coupon id from auto apply coupon list if auto apply is disabled.
if ( is_array( $auto_apply_coupon_ids ) && in_array( $post_id, $auto_apply_coupon_ids, true ) ) {
$auto_apply_coupon_ids = array_diff( $auto_apply_coupon_ids, array( $post_id ) );
}
}
update_option( 'wc_sc_auto_apply_coupon_ids', $auto_apply_coupon_ids, 'no' );
}
/**
* Add meta in export headers
*
* @param array $headers Existing headers.
* @return array
*/
public function export_headers( $headers = array() ) {
$headers['wc_sc_auto_apply_coupon'] = __( 'Auto apply?', 'woocommerce-smart-coupons' );
return $headers;
}
/**
* Post meta defaults for auto apply coupon meta
*
* @param array $defaults Existing postmeta defaults.
* @return array $defaults Modified postmeta defaults
*/
public function postmeta_defaults( $defaults = array() ) {
$defaults['wc_sc_auto_apply_coupon'] = '';
return $defaults;
}
/**
* Add auto apply coupon's meta with value in coupon meta
*
* @param array $data The row data.
* @param array $post The POST values.
* @return array $data Modified row data
*/
public function generate_coupon_meta( $data = array(), $post = array() ) {
if ( isset( $post['discount_type'] ) && 'smart_coupon' !== $post['discount_type'] ) {
$data['wc_sc_auto_apply_coupon'] = ( isset( $post['wc_sc_auto_apply_coupon'] ) ) ? $post['wc_sc_auto_apply_coupon'] : '';
}
return $data;
}
/**
* Process coupon meta value for import
*
* @param mixed $meta_value The meta value.
* @param array $args Additional Arguments.
* @return mixed $meta_value
*/
public function process_coupon_meta_value_for_import( $meta_value = null, $args = array() ) {
$discount_type = isset( $args['discount_type'] ) ? $args['discount_type'] : '';
if ( 'smart_coupon' !== $discount_type && ! empty( $args['meta_key'] ) && 'wc_sc_auto_apply_coupon' === $args['meta_key'] ) {
$auto_apply_coupon = $meta_value;
if ( 'yes' === $auto_apply_coupon ) {
$auto_apply_coupon_ids = get_option( 'wc_sc_auto_apply_coupon_ids', array() );
$auto_apply_coupon_ids = ( empty( $auto_apply_coupon_ids ) || ! is_array( $auto_apply_coupon_ids ) ) ? array() : $auto_apply_coupon_ids;
$auto_apply_coupon_ids = array_map( 'absint', $auto_apply_coupon_ids );
$coupon_id = ( isset( $args['post']['post_id'] ) ) ? absint( $args['post']['post_id'] ) : 0;
if ( ! empty( $coupon_id ) && ! in_array( $coupon_id, $auto_apply_coupon_ids, true ) ) {
$auto_apply_coupon_ids[] = $coupon_id;
update_option( 'wc_sc_auto_apply_coupon_ids', $auto_apply_coupon_ids, 'no' );
}
}
}
return $meta_value;
}
/**
* Make meta data of auto apply coupon meta protected
*
* @param bool $protected Is protected.
* @param string $meta_key The meta key.
* @param string $meta_type The meta type.
* @return bool $protected
*/
public function make_action_meta_protected( $protected = false, $meta_key = '', $meta_type = '' ) {
if ( 'wc_sc_auto_apply_coupon' === $meta_key ) {
return true;
}
return $protected;
}
/**
* Get auto applied coupons
*
* @since 4.27.0
* @return array
*/
public function get_auto_applied_coupons() {
$coupons = ( is_object( WC()->session ) && is_callable( array( WC()->session, 'get' ) ) ) ? WC()->session->get( 'wc_sc_auto_applied_coupons' ) : array();
$coupons = ( ! empty( $coupons ) && is_array( $coupons ) ) ? array_filter( array_unique( $coupons ) ) : array();
return apply_filters( 'wc_sc_' . __FUNCTION__, $coupons, array( 'source' => $this ) );
}
/**
* Add auto applied coupon to WC session
*
* @since 4.27.0
* @param string $coupon_code Coupon Code.
*/
public function set_auto_applied_coupon( $coupon_code = '' ) {
if ( ! empty( $coupon_code ) ) {
$coupons = $this->get_auto_applied_coupons();
// Check if auto applied coupons are not empty.
if ( ! empty( $coupons ) && is_array( $coupons ) ) {
$coupons[] = $coupon_code;
} else {
$coupons = array( $coupon_code );
}
if ( is_object( WC()->session ) && is_callable( array( WC()->session, 'set' ) ) ) {
WC()->session->set( 'wc_sc_auto_applied_coupons', $coupons );
}
}
}
/**
* Remove an auto applied coupon from WC session
*
* @since 4.31.0
* @param string $coupon_code Coupon Code.
*/
public function unset_auto_applied_coupon( $coupon_code = '' ) {
if ( ! empty( $coupon_code ) ) {
$update = false;
$coupons = $this->get_auto_applied_coupons();
// Check if auto applied coupons are not empty.
if ( ! empty( $coupons ) && in_array( $coupon_code, $coupons, true ) ) {
$coupons = array_diff( $coupons, array( $coupon_code ) );
$update = true;
}
if ( true === $update && is_object( WC()->session ) && is_callable( array( WC()->session, 'set' ) ) ) {
$coupons = array_values( array_filter( $coupons ) );
WC()->session->set( 'wc_sc_auto_applied_coupons', $coupons );
}
}
}
/**
* Reset cart session data.
*
* @since 4.27.0
*/
public function reset_auto_applied_coupons_session() {
if ( is_object( WC()->session ) && is_callable( array( WC()->session, 'set' ) ) ) {
WC()->session->set( 'wc_sc_auto_applied_coupons', null );
}
}
/**
* Runs after a coupon is removed
*
* @since 4.31.0
* @param string $coupon_code The coupon code.
* @return void
*/
public function wc_sc_removed_coupon( $coupon_code = '' ) {
$backtrace = debug_backtrace( DEBUG_BACKTRACE_IGNORE_ARGS ); // phpcs:ignore
$is_automatic = true;
if ( ! empty( $backtrace ) ) {
foreach ( $backtrace as $trace ) {
if (
isset( $trace['file'] ) &&
(
false !== strpos( $trace['file'], 'StoreApi/Routes/V1/CartRemoveCoupon.php' ) ||
( ! empty( $trace['function'] ) && 'remove_coupon' === $trace['function'] && ! empty( $trace['class'] ) && 'WC_AJAX' === $trace['class'] )
)
) {
if ( false !== strpos( $trace['file'], 'StoreApi/Routes/V1/CartRemoveCoupon.php' ) ) {
// Call auto_apply_coupons() if removal is from StoreApi/Routes/V1/CartRemoveCoupon.php.
$this->auto_apply_coupons();
return; // Exit function after calling auto_apply_coupons().
}
// Coupon was removed by the user.
$is_automatic = false;
break; // Exit loop once the user removal is identified.
}
}
}
if ( $is_automatic ) {
$this->unset_auto_applied_coupon( $coupon_code );
}
}
/**
* Check if auto apply coupon allowed in the cart
*
* @since 4.27.0
* @return bool.
*/
public function is_allow_auto_apply_coupons() {
$auto_applied_coupons = $this->get_auto_applied_coupons();
$auto_applied_coupons_count = ! empty( $auto_applied_coupons ) && is_array( $auto_applied_coupons ) ? count( $auto_applied_coupons ) : 0;
$max_auto_apply_coupons_limit = apply_filters( 'wc_sc_max_auto_apply_coupons_limit', get_option( 'wc_sc_max_auto_apply_coupons_limit', 5 ), array( 'source' => $this ) );
return apply_filters(
'wc_sc_' . __FUNCTION__,
$auto_applied_coupons_count < $max_auto_apply_coupons_limit,
array(
'source' => $this,
'auto_applied_coupons' => $auto_applied_coupons,
)
);
}
/**
* Check if the auto apply removable
*
* @since 4.27.0
* @param string $coupon_code Coupon Code.
* @return bool.
*/
public function is_auto_apply_coupon_removable( $coupon_code = '' ) {
return apply_filters(
'wc_sc_' . __FUNCTION__,
get_option( 'wc_sc_auto_apply_coupon_removable', 'yes' ),
array(
'source' => $this,
'coupon_code' => $coupon_code,
)
);
}
/**
* Check if the coupon is applied through auto apply
*
* @since 4.27.0
* @param string $coupon_code Coupon Code.
* @return bool.
*/
public function is_coupon_applied_by_auto_apply( $coupon_code = '' ) {
if ( ! empty( $coupon_code ) ) {
$applied_coupons = $this->get_auto_applied_coupons();
if ( ! empty( $applied_coupons ) && is_array( $applied_coupons ) && in_array( $coupon_code, $applied_coupons, true ) ) {
return true;
}
}
return false;
}
/**
* Check if coupon is applicable for auto apply
*
* @since 4.26.0
* @param WC_Coupon $coupon WooCommerce coupon object.
* @return bool
*/
public function is_coupon_valid_for_auto_apply( $coupon = null ) {
$valid = false;
if ( is_object( $coupon ) && $coupon instanceof WC_Coupon ) {
if ( $this->is_wc_gte_30() ) {
$coupon_code = is_callable( array( $coupon, 'get_code' ) ) ? $coupon->get_code() : '';
$discount_type = is_callable( array( $coupon, 'get_discount_type' ) ) ? $coupon->get_discount_type() : '';
$is_auto_generate_coupon = is_callable( array( $coupon, 'get_meta' ) ) ? $coupon->get_meta( 'auto_generate_coupon' ) : 'no';
$is_disable_email_restrict = is_callable( array( $coupon, 'get_meta' ) ) ? $coupon->get_meta( 'sc_disable_email_restriction' ) : 'no';
} else {
$coupon_id = ( ! empty( $coupon->id ) ) ? $coupon->id : 0;
$coupon_code = ( ! empty( $coupon->code ) ) ? $coupon->code : '';
$discount_type = get_post_meta( $coupon_id, 'discount_type', true );
$is_auto_generate_coupon = get_post_meta( $coupon_id, 'auto_generate_coupon', true );
$is_disable_email_restrict = get_post_meta( $coupon_id, 'sc_disable_email_restriction', true );
}
$is_removable = $this->is_auto_apply_coupon_removable( $coupon_code );
$is_auto_applied = $this->is_coupon_applied_by_auto_apply( $coupon_code );
/**
* Validate coupon for auto apply if
*
* Discount type is not smart_coupon.
* Auto generate is not enabled.
* Coupon should not be auto applied OR auto applied coupon should not be removable.
* Coupon code is valid.
*/
$valid = 'smart_coupon' !== $discount_type
&& 'yes' !== $is_auto_generate_coupon
&& ( ! $is_auto_applied || 'yes' !== $is_removable )
&& $this->is_valid( $coupon );
}
return apply_filters(
'wc_sc_' . __FUNCTION__,
$valid,
array(
'coupon_obj' => $coupon,
'source' => $this,
)
);
}
/**
* Function to apply coupons automatically.
*
* TODO: IF we need another variable for removed coupons;
* There will be 2 session variables: wc_sc_auto_applied_coupons and wc_sc_removed_auto_applied_coupons.
* Whenever a coupon will be auto-applied, it'll be stored in wc_sc_auto_applied_coupons.
* Whenever a coupon will be removed, it'll be moved from wc_sc_auto_applied_coupons to wc_sc_removed_auto_applied_coupons.
* And before applying an auto-apply coupon, it'll be made sure that the coupon doesn't exist in wc_sc_removed_auto_applied_coupons
* And sum of counts of both session variable will be considered before auto applying coupons. It will be made sure that the sum of counts in not exceeding option `wc_sc_max_auto_apply_coupons_limit`
* Reference: issues/234#note_27085
*/
public function auto_apply_coupons() {
$cart = ( is_object( WC() ) && isset( WC()->cart ) ) ? WC()->cart : null;
if ( is_object( $cart ) && is_callable( array( $cart, 'is_empty' ) ) && ! $cart->is_empty() && $this->is_allow_auto_apply_coupons() ) {
global $wpdb;
$user_role = '';
$email = '';
if ( ! is_admin() ) {
$current_user = wp_get_current_user();
if ( ! empty( $current_user->ID ) ) {
$max_user_roles_limit = apply_filters( 'wc_sc_max_user_roles_limit', 5 );
$user_roles = ( ! empty( $current_user->roles ) ) ? $current_user->roles : array();
if ( count( $user_roles ) > $max_user_roles_limit ) {
$user_roles = array_slice( $user_roles, 0, $max_user_roles_limit );
}
$email = get_user_meta( $current_user->ID, 'billing_email', true );
$email = ( ! empty( $email ) ) ? $email : $current_user->user_email;
}
}
$query = $wpdb->prepare(
"SELECT DISTINCT p.ID
FROM {$wpdb->posts} AS p
JOIN {$wpdb->postmeta} AS pm1
ON (p.ID = pm1.post_id
AND p.post_type = %s
AND p.post_status = %s
AND pm1.meta_key = %s
AND pm1.meta_value = %s)
JOIN {$wpdb->postmeta} AS pm2
ON (p.ID = pm2.post_id
AND pm2.meta_key IN ('wc_sc_user_role_ids', 'customer_email')
AND (pm2.meta_value = ''
OR pm2.meta_value = 'a:0:{}'",
'shop_coupon',
'publish',
'wc_sc_auto_apply_coupon',
'yes'
);
if ( ! empty( $user_roles ) ) {
foreach ( $user_roles as $user_role ) {
$query .= $wpdb->prepare(
' OR pm2.meta_value LIKE %s',
'%' . $wpdb->esc_like( $user_role ) . '%'
);
}
}
if ( ! empty( $email ) ) {
$query .= $wpdb->prepare(
' OR pm2.meta_value LIKE %s',
'%' . $wpdb->esc_like( $email ) . '%'
);
}
$query .= '))';
$auto_apply_coupon_ids = $wpdb->get_col( $query ); // phpcs:ignore
$auto_apply_coupon_ids = ( empty( $auto_apply_coupon_ids ) || ! is_array( $auto_apply_coupon_ids ) ) ? array() : $auto_apply_coupon_ids;
$auto_apply_coupon_ids = array_filter( array_map( 'absint', $auto_apply_coupon_ids ) );
if ( ! empty( $auto_apply_coupon_ids ) && is_array( $auto_apply_coupon_ids ) ) {
$valid_coupon_counter = 0;
$max_auto_apply_coupons_limit = apply_filters( 'wc_sc_max_auto_apply_coupons_limit', get_option( 'wc_sc_max_auto_apply_coupons_limit', 5 ), array( 'source' => $this ) );
$current_filter = current_filter();
do_action(
'wc_sc_before_auto_apply_coupons',
array(
'source' => $this,
'current_filter' => $current_filter,
)
);
foreach ( $auto_apply_coupon_ids as $apply_coupon_id ) {
// Process only five coupons.
if ( absint( $max_auto_apply_coupons_limit ) === $valid_coupon_counter ) {
break;
}
$coupon = new WC_Coupon( absint( $apply_coupon_id ) );
if ( $this->is_wc_gte_30() ) {
$coupon_id = ( ! empty( $coupon ) && is_callable( array( $coupon, 'get_id' ) ) ) ? $coupon->get_id() : 0;
$coupon_code = ( ! empty( $coupon ) && is_callable( array( $coupon, 'get_code' ) ) ) ? $coupon->get_code() : '';
} else {
$coupon_id = ( ! empty( $coupon->id ) ) ? $coupon->id : 0;
$coupon_code = ( ! empty( $coupon->code ) ) ? $coupon->code : '';
}
// If coupon has payment method restriction and already store wc_sc_auto_applied_coupons.
$payment_method_ids = $this->get_post_meta( $coupon_id, 'wc_sc_payment_method_ids', true );
if ( ( is_array( $payment_method_ids ) && count( $payment_method_ids ) > 0 ) && $this->is_coupon_applied_by_auto_apply( $coupon_code ) ) {
$this->unset_auto_applied_coupon( $coupon_code );
}
// Check if it is a valid coupon object.
if ( $apply_coupon_id === $coupon_id && ! empty( $coupon_code ) && $this->is_coupon_valid_for_auto_apply( $coupon ) ) {
$cart_total = ( $this->is_wc_greater_than( '3.1.2' ) ) ? $cart->get_cart_contents_total() : $cart->cart_contents_total;
$is_auto_apply = apply_filters(
'wc_sc_is_auto_apply',
( $cart_total > 0 ),
array(
'source' => $this,
'cart_obj' => $cart,
'coupon_obj' => $coupon,
'cart_total' => $cart_total,
)
);
// Check if cart still requires a coupon discount and does not have coupon already applied.
if ( true === $is_auto_apply && ! $cart->has_discount( $coupon_code ) ) {
$cart->add_discount( $coupon_code );
$cart->calculate_shipping();
$cart->calculate_totals();
$this->set_auto_applied_coupon( $coupon_code );
}
$valid_coupon_counter++;
} // End if to check valid coupon.
}
}
}
}
/**
* Function to apply coupons for cart and checkout block.
*
* @param bool $pre_render Is protected.
* @param array $parsed_block The meta key.
* @param array $parent_block The meta type.
* @return bool
*/
public function auto_apply_coupons_to_cart_checkout_block( $pre_render = null, $parsed_block = array(), $parent_block = null ) {
if ( isset( $parsed_block['blockName'] ) && in_array( $parsed_block['blockName'], array( 'woocommerce/cart', 'woocommerce/checkout' ), true ) ) {
$this->auto_apply_coupons();
}
return $pre_render;
}
/**
* Function to Automatically Apply Coupons on Cart Update.
*
* @param string $cart_item_key contains the id of the cart item. This may be empty if the cart item does not exist any more.
* @param int $quantity contains the quantity of the item.
* @param WC_Cart $cart Cart class.
*/
public function auto_apply_coupons_on_cart_update( $cart_item_key, $quantity, $cart ) {
if ( WC()->is_rest_api_request() ) {
$this->auto_apply_coupons();
}
}
/**
* Automatically apply coupons during the WooCommerce checkout order review update process.
* This function checks for changes in the shipping method and updates the chosen shipping method in the session.
* Then, it automatically applies any eligible coupons.
*/
public function wc_checkout_update_order_review_auto_apply_coupons() {
check_ajax_referer( 'update-order-review', 'security' );
// Get the posted shipping methods from the form data, if available.
$posted_shipping_methods = isset( $_POST['shipping_method'] ) ? wc_clean( wp_unslash( $_POST['shipping_method'] ) ) : array(); // phpcs:ignore
if ( ! empty( $posted_shipping_methods ) ) {
// Retrieve the current chosen shipping methods from the session.
$chosen_shipping_methods = WC()->session->get( 'chosen_shipping_methods' );
if ( is_array( $posted_shipping_methods ) ) {
// Update the chosen shipping methods with the posted values.
foreach ( $posted_shipping_methods as $i => $value ) {
if ( ! is_string( $value ) ) {
continue;
}
$chosen_shipping_methods[ $i ] = $value;
}
}
// Save the updated chosen shipping methods back to the session.
WC()->session->set( 'chosen_shipping_methods', $chosen_shipping_methods );
}
// Automatically apply any eligible coupons.
$this->auto_apply_coupons();
}
/**
* Remove coupons that result in zero discount.
*
* This method loops through all applied coupons in the cart and checks if they are
* not present in the coupon discount totals. If a coupon is found to have a zero
* discount (i.e., it is not in the coupon discount totals), it is removed from the
* applied coupons list and the auto-applied coupon logic is handled.
*
* @param WC_Cart $cart The WooCommerce cart object.
*/
public function remove_coupon_if_zero( $cart ) {
// Get all applied coupons.
$applied_coupons = $cart->get_applied_coupons();
if ( ! empty( $applied_coupons ) ) {
foreach ( $applied_coupons as $coupon_code ) {
// Check if the coupon is not in the coupon discount totals (indicating a zero discount).
if ( ! array_key_exists( $coupon_code, $cart->get_coupon_discount_totals() ) ) {
// Remove the coupon from the applied coupons array.
$updated_coupons = array_diff( $applied_coupons, array( $coupon_code ) );
$cart->set_applied_coupons( $updated_coupons );
// Unset the auto-applied coupon from session.
$this->unset_auto_applied_coupon( $coupon_code );
}
}
}
}
/**
* Automatically apply coupons to the cart when the shipping method is changed.
* This method checks if the request is an AJAX request to update the shipping method,
* and if so, it calls the auto_apply_coupons method to apply any eligible coupons.
*/
public function auto_apply_coupons_to_cart_on_shipping_change() {
if ( is_ajax() ) {
// Check if the request is an AJAX request to update the shipping method.
check_ajax_referer( 'update-shipping-method', 'security' );
$this->auto_apply_coupons();
}
}
/**
* Handle auto apply related hooks
*/
public function handle_auto_apply_hooks() {
$is_allow_auto_apply = $this->sc_get_option( 'wc_sc_allow_auto_apply', 'yes' );
if ( 'yes' === $is_allow_auto_apply ) {
// Action to auto apply coupons.
add_action( 'woocommerce_cart_is_empty', array( $this, 'auto_apply_coupons' ) );
add_action( 'woocommerce_shortcode_before_product_cat_loop', array( $this, 'auto_apply_coupons' ) );
add_action( 'woocommerce_before_shop_loop', array( $this, 'auto_apply_coupons' ) );
add_action( 'woocommerce_before_single_product', array( $this, 'auto_apply_coupons' ) );
add_action( 'woocommerce_before_cart', array( $this, 'auto_apply_coupons' ) );
add_action( 'woocommerce_before_checkout_form', array( $this, 'auto_apply_coupons' ) );
add_action( 'woocommerce_account_content', array( $this, 'auto_apply_coupons' ) );
add_action( 'woocommerce_checkout_update_order_review', array( $this, 'wc_checkout_update_order_review_auto_apply_coupons' ) );
add_action( 'woocommerce_cart_emptied', array( $this, 'reset_auto_applied_coupons_session' ) );
add_action( 'woocommerce_removed_coupon', array( $this, 'blocks_removed_coupon' ), 20 );
add_action( 'woocommerce_removed_coupon', array( $this, 'wc_sc_removed_coupon' ), 99 );
add_filter( 'pre_render_block', array( $this, 'auto_apply_coupons_to_cart_checkout_block' ), 10, 3 );
add_action( 'woocommerce_cart_item_set_quantity', array( $this, 'auto_apply_coupons_on_cart_update' ), 10, 4 );
add_action( 'woocommerce_before_cart_totals', array( $this, 'auto_apply_coupons_to_cart_on_shipping_change' ) );
}
}
/**
* Recheck coupon removed from auto-apply
*
* @param string $code The coupon code.
*/
public function blocks_removed_coupon( $code = '' ) {
if ( empty( $code ) ) {
return;
}
if ( function_exists( 'WC' ) && isset( WC()->cart ) && is_a( WC()->cart, 'WC_Cart' ) ) {
$coupon = new WC_Coupon( $code );
$is_removable = $this->is_auto_apply_coupon_removable( $code );
$is_auto_applied = $this->is_coupon_applied_by_auto_apply( $code );
if ( true === $is_auto_applied && 'yes' !== $is_removable && $this->is_callable( WC()->cart, 'add_discount' ) ) {
WC()->cart->add_discount( $code );
}
}
}
}
}
WC_SC_Auto_Apply_Coupon::get_instance();

View File

@@ -0,0 +1,433 @@
<?php
/**
* WooCommerce Smart Coupon DB update.
*
* @author StoreApps
* @since 4.28.0
* @version 1.1.1
* @package woocommerce-smart-coupons/includes/
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
if ( ! class_exists( 'WC_SC_Background_Upgrade' ) ) {
/**
* Class for WooCommerce Smart Coupons database update.
*/
class WC_SC_Background_Upgrade {
/**
* Number of row to fetch once a query run.
*
* (default value: 100)
*
* @var int
* @access protected
*/
protected $row_limit = 100;
/**
* Action name.
*
* @var string
* @access protected
*/
protected $action = '';
/**
* Variable to hold instance of WC_SC_Background_Upgrade
*
* @var $instance
*/
private static $instance = null;
/**
* Get single instance of WC_SC_Background_Upgrade
*
* @return WC_SC_Background_Upgrade Singleton object of WC_SC_Background_Upgrade
*/
public static function get_instance() {
// Check if instance is already exists.
if ( is_null( self::$instance ) ) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Handle call to functions which is not available in this class
*
* @param string $function_name The function name.
* @param array $arguments Array of arguments passed while calling $function_name.
* @return result of function call
*/
public function __call( $function_name, $arguments = array() ) {
global $woocommerce_smart_coupon;
if ( ! is_callable( array( $woocommerce_smart_coupon, $function_name ) ) ) {
return;
}
if ( ! empty( $arguments ) ) {
return call_user_func_array( array( $woocommerce_smart_coupon, $function_name ), $arguments );
} else {
return call_user_func( array( $woocommerce_smart_coupon, $function_name ) );
}
}
/**
* Example_Background_Processing constructor.
*/
private function __construct() {
$this->action = 'wc_db_upgrade';
add_action( 'plugins_loaded', array( $this, 'init' ) );
add_action( 'init', array( $this, 'process_handler' ) );
add_action( 'init', array( $this, 'clear_all_process' ) );
add_action( 'action_scheduler_failed_action', array( $this, 'restart_failed_action' ) );
}
/**
* Init
*/
public function init() {
global $woocommerce_smart_coupon;
// Get list of db updates.
$updates = $this->get_updates();
if ( ! empty( $updates ) ) {
foreach ( $updates as $update ) {
// Break if version is empty.
if ( empty( $update['version'] ) ) {
break;
}
$version = $update['version'];
$update_status = $this->get_status( $version );
if ( version_compare( $woocommerce_smart_coupon->get_smart_coupons_version(), $version, '>=' ) && ( false === $update_status ) ) {
// Set db update status to pending.
$this->set_status( $version, 'pending' );
}
$handler = isset( $update['cron_handler'] ) ? $update['cron_handler'] : '';
$this->register_scheduler( $handler );
}
}
}
/**
* Process handler
*/
public function process_handler() {
if ( ! isset( $_GET['wc_sc_update'] ) || ! isset( $_GET['wc_sc_db_update_nonce'] ) ) {
return;
}
if ( ! wp_verify_nonce( wc_clean( wp_unslash( $_GET['wc_sc_db_update_nonce'] ) ), 'wc_sc_db_process' ) ) { // phpcs:ignore
return;
}
$this->handle_all( wc_clean( wp_unslash( $_GET['wc_sc_update'] ) ) ); // phpcs:ignore
}
/**
* Get list of db updates.
*/
public function get_updates() {
return array(
array(
'version' => '4.28.0', // Minimum plugin version to do the action.
'get_row_handler' => array( __CLASS__, 'get_applied_coupon_profile_options' ), // get data.
'cron_handler' => 'wcsc_move_applied_coupon_options_to_transient', // define cron handler which should be in this class.
),
);
}
/**
* Register action schedulers for db update.
*
* @param string $handler Handler name.
*/
public function register_scheduler( $handler = '' ) {
if ( ! empty( $handler ) && is_callable( array( $this, $handler ) ) ) {
add_action( $handler, array( $this, $handler ) );
}
}
/**
* Handle all updates.
*
* @param string $current_version Plugin version.
*/
protected function handle_all( $current_version = '0' ) {
if ( empty( $current_version ) ) {
return;
}
do_action( 'wc_sc_start_background_update', $current_version );
$updates = $this->get_updates();
if ( is_array( $updates ) && ! empty( $updates ) ) {
foreach ( $updates as $update ) {
// Compare version for db updates.
if ( ! empty( $update['version'] ) && version_compare( $current_version, $update['version'], '>=' ) ) {
$cron_handler = isset( $update['cron_handler'] ) ? $update['cron_handler'] : '';
if ( ! empty( $cron_handler ) && is_callable( array( $this, $cron_handler ) ) ) {
$row_handler = isset( $update['get_row_handler'] ) ? $update['get_row_handler'] : '';
$this->process( $update['version'], $cron_handler, $row_handler );
}
}
}
}
}
/**
* Handle the process.
*
* @param string $version Version number.
* @param string $cron_handler Cron handler action name.
* @param string $row_handler Fetch data handler name.
*/
private function process( $version = '', $cron_handler = '', $row_handler = '' ) {
$rows = ! empty( $row_handler ) && is_callable( $row_handler ) ? call_user_func( $row_handler ) : '';
if ( ! empty( $rows ) && is_array( $rows ) ) {
// Start the process if the status is pending.
if ( 'pending' === $this->get_status( $version ) ) {
if ( function_exists( 'as_enqueue_async_action' ) ) {
$this->set_status( $version, 'processing' );
as_enqueue_async_action( $cron_handler );
}
}
} else {
// Set status to `completed` if the data is empty.
$this->set_status( $version, 'completed' );
}
}
/**
* Clear all update process.
*/
public function clear_all_process() {
$updates = $this->get_updates();
foreach ( $updates as $update ) {
$version = isset( $update['version'] ) ? $update['version'] : '';
$row_handler = isset( $update['get_row_handler'] ) ? $update['get_row_handler'] : '';
$status = $this->get_status( $version );
if ( false === $status || 'done' === $status ) {
return;
}
$rows = ! empty( $row_handler ) && is_callable( $row_handler ) ? call_user_func( $row_handler ) : '';
if ( 'processing' === $status && empty( $rows ) ) {
do_action( 'wc_sc_background_update_completed', $version );
$this->set_status( $version, 'completed' );
}
}
}
/**
* Callback Method for move options to transient.
*
* @return void
*/
public function wcsc_move_applied_coupon_options_to_transient() {
$options = $this->get_applied_coupon_profile_options();
// Check if coupons are not empty.
if ( ! empty( $options ) && is_array( $options ) ) {
$start_time = time();
$loop = 1;
foreach ( $options as $option ) {
// disable auto apply.
$this->move_option_to_transient( $option );
if ( $this->loop_exceeded( $loop ) || $this->time_exceeded( $start_time ) || $this->memory_exceeded() ) {
// Update auto apply coupon id list.
if ( function_exists( 'as_enqueue_async_action' ) ) {
as_enqueue_async_action( __FUNCTION__ );
}
break;
}
$loop++;
}
}
}
/**
* Restart scheduler after one minute if it fails
*
* @param array $action_id id of failed action.
*/
public function restart_failed_action( $action_id = 0 ) {
if ( empty( $action_id ) || ! class_exists( 'ActionScheduler' ) || ! is_callable( array( 'ActionScheduler', 'store' ) ) || ! function_exists( 'as_enqueue_async_action' ) ) {
return;
}
$action = ActionScheduler::store()->fetch_action( $action_id );
$action_hook = $action->get_hook();
$updates = $this->get_updates();
if ( ! empty( $updates ) ) {
foreach ( $updates as $update ) {
if ( ! empty( $update['version'] ) && ! empty( $update['cron_handler'] ) ) {
if ( $action_hook === $update['cron_handler'] ) {
$this->set_status( $update['version'], 'processing' );
as_enqueue_async_action( $update['cron_handler'] );
}
}
}
}
}
/**
* Method to update the status of db upgrade.
*
* @param string $version wcsc db version.
* @param string $status status.
*
* @return bool.
*/
public function set_status( $version = '', $status = '' ) {
if ( ! empty( $version ) && ! empty( $status ) ) {
$db_status = get_option( 'sc_wc_db_update_status', array() );
$db_status[ $version ] = $status;
return update_option( 'sc_wc_db_update_status', $db_status, 'no' );
}
return false;
}
/**
* Method to get the status of db upgrade.
*
* @param string $version wcsc db version.
*
* @return bool.
*/
public function get_status( $version = '' ) {
if ( ! empty( $version ) ) {
$db_status = get_option( 'sc_wc_db_update_status', array() );
return ( ! empty( $db_status ) && isset( $db_status[ $version ] ) ) ? $db_status[ $version ] : false;
}
return false;
}
/**
* Loop exceeded
*
* Ensures the batch process never exceeds to handle the given limit of row.
*
* @param int $loop Number of loop done.
* @return bool
*/
public function loop_exceeded( $loop = 0 ) {
return $loop > apply_filters( $this->action . '_default_row_limit', $this->row_limit );
}
/**
* Memory exceeded
*
* Ensures the batch process never exceeds 90%
* of the maximum WordPress memory.
*
* @return bool
*/
protected function memory_exceeded() {
$memory_limit = $this->get_memory_limit() * 0.9; // 90% of max memory
$current_memory = memory_get_usage( true );
if ( $current_memory >= $memory_limit ) {
return true;
}
return false;
}
/**
* Get memory limit.
*
* @return int
*/
protected function get_memory_limit() {
if ( function_exists( 'ini_get' ) ) {
$memory_limit = ini_get( 'memory_limit' );
} else {
// Sensible default.
$memory_limit = '128M';
}
if ( ! $memory_limit || -1 === intval( $memory_limit ) ) {
// Unlimited, set to 32GB.
$memory_limit = '32G';
}
return wp_convert_hr_to_bytes( $memory_limit );
}
/**
* Time exceeded.
*
* Ensures the batch never exceeds a sensible time limit.
* A timeout limit of 30s is common on shared hosting.
*
* @param string $start_time start timestamp.
* @return bool
*/
protected function time_exceeded( $start_time = '' ) {
$finish = $start_time + apply_filters( $this->action . '_default_time_limit', 20 ); // 20 seconds
$return = false;
if ( time() >= $finish ) {
$return = true;
}
return apply_filters( $this->action . '_time_exceeded', $return );
}
/**
* Get `applied_coupon_profile` options names and values
*
* @return array
*/
public function get_applied_coupon_profile_options() {
global $wpdb;
$option_name = 'sc_applied_coupon_profile_%';
$options = $wpdb->get_results( // @codingStandardsIgnoreLine
$wpdb->prepare(
"SELECT option_name, option_value
FROM $wpdb->options
WHERE option_name LIKE %s",
$option_name
),
ARRAY_A
);
return $options;
}
/**
* Method to transfer single options to transient.
*
* @param array $option Array of option name and value.
* @return void
*/
protected function move_option_to_transient( $option = array() ) {
if ( isset( $option['option_name'] ) && isset( $option['option_value'] ) ) {
// Add new transient with option name.
$move = set_transient(
$option['option_name'],
maybe_unserialize( $option['option_value'] ),
apply_filters( 'wc_sc_applied_coupon_by_url_expire_time', MONTH_IN_SECONDS )
);
if ( true === $move ) {
// Delete the option if option is successfully moved.
delete_option( $option['option_name'] );
}
}
}
} // End class
} // End class exists check
WC_SC_Background_Upgrade::get_instance();

View File

@@ -0,0 +1,865 @@
<?php
/**
* Handle coupon actions
*
* @author StoreApps
* @since 3.5.0
* @version 1.10.0
*
* @package woocommerce-smart-coupons/includes/
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
if ( ! class_exists( 'WC_SC_Coupon_Actions' ) ) {
/**
* Class for handling processes of coupons
*/
class WC_SC_Coupon_Actions {
/**
* Variable to hold instance of WC_SC_Coupon_Actions
*
* @var $instance
*/
private static $instance = null;
/**
* Constructor
*/
private function __construct() {
add_filter( 'woocommerce_add_cart_item', array( $this, 'modify_cart_item_data_in_add_to_cart' ), 15, 2 );
add_filter( 'woocommerce_get_cart_item_from_session', array( $this, 'modify_cart_item_in_session' ), 15, 3 );
add_filter( 'woocommerce_cart_item_quantity', array( $this, 'modify_cart_item_quantity' ), 5, 3 );
add_filter( 'woocommerce_cart_item_price', array( $this, 'modify_cart_item_price' ), 10, 3 );
add_filter( 'woocommerce_cart_item_subtotal', array( $this, 'modify_cart_item_price' ), 10, 3 );
add_filter( 'woocommerce_order_formatted_line_subtotal', array( $this, 'modify_cart_item_price' ), 10, 3 );
add_filter( 'woocommerce_coupon_get_items_to_validate', array( $this, 'remove_products_from_validation' ), 10, 2 );
add_action( 'woocommerce_applied_coupon', array( $this, 'coupon_action' ) );
add_action( 'woocommerce_removed_coupon', array( $this, 'remove_product_from_cart' ) );
add_action( 'woocommerce_check_cart_items', array( $this, 'review_cart_items' ) );
add_action( 'woocommerce_checkout_create_order_line_item', array( $this, 'add_product_source_in_order_item_meta' ), 10, 4 );
add_filter( 'wc_smart_coupons_export_headers', array( $this, 'export_headers' ) );
add_filter( 'wc_sc_export_coupon_meta_data', array( $this, 'export_coupon_meta_data' ), 10, 2 );
add_filter( 'smart_coupons_parser_postmeta_defaults', array( $this, 'postmeta_defaults' ) );
add_filter( 'sc_generate_coupon_meta', array( $this, 'generate_coupon_meta' ), 10, 2 );
add_filter( 'is_protected_meta', array( $this, 'make_action_meta_protected' ), 10, 3 );
add_action( 'wc_sc_new_coupon_generated', array( $this, 'copy_coupon_action_meta' ) );
add_filter( 'show_zero_amount_coupon', array( $this, 'show_coupon_with_actions' ), 10, 2 );
add_filter( 'wc_sc_is_auto_generate', array( $this, 'auto_generate_coupon_with_actions' ), 10, 2 );
add_filter( 'wc_sc_validate_coupon_amount', array( $this, 'validate_coupon_amount' ), 10, 2 );
add_filter( 'wc_sc_hold_applied_coupons', array( $this, 'maybe_run_coupon_actions' ), 10, 2 );
add_action( 'woocommerce_after_cart_item_quantity_update', array( $this, 'stop_cart_item_quantity_update' ), 99, 4 );
add_action( 'woocommerce_order_applied_coupon', array( $this, 'coupon_action' ), 10, 2 );
add_action( 'admin_init', array( $this, 'remove_product_from_order' ), 11 );
}
/**
* Get single instance of WC_SC_Coupon_Actions
*
* @return WC_SC_Coupon_Actions Singleton object of WC_SC_Coupon_Actions
*/
public static function get_instance() {
// Check if instance is already exists.
if ( is_null( self::$instance ) ) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Handle call to functions which is not available in this class
*
* @param string $function_name The function name.
* @param array $arguments Array of arguments passed while calling $function_name.
* @return result of function call
*/
public function __call( $function_name, $arguments = array() ) {
global $woocommerce_smart_coupon;
if ( ! is_callable( array( $woocommerce_smart_coupon, $function_name ) ) ) {
return;
}
if ( ! empty( $arguments ) ) {
return call_user_func_array( array( $woocommerce_smart_coupon, $function_name ), $arguments );
} else {
return call_user_func( array( $woocommerce_smart_coupon, $function_name ) );
}
}
/**
* Get coupon actions
*
* @param string $coupon_code The coupon code.
* @return array Coupon actions
*/
public function get_coupon_actions( $coupon_code = '' ) {
if ( empty( $coupon_code ) ) {
return array();
}
$coupon_code = wc_format_coupon_code( $coupon_code );
$coupons = get_posts(
array(
'post_type' => 'shop_coupon',
'title' => $coupon_code,
'post_status' => 'publish',
'numberposts' => 1,
)
);
$coupon = current( $coupons );
$coupon_id = ( ! empty( $coupon->ID ) ) ? $coupon->ID : 0;
$coupon = new WC_Coupon( $coupon_id );
if ( ! is_wp_error( $coupon ) ) {
if ( $this->is_wc_gte_30() ) {
$actions = $coupon->get_meta( 'wc_sc_add_product_details' );
} else {
$coupon_id = ( ! empty( $coupon->id ) ) ? $coupon->id : 0;
$actions = get_post_meta( $coupon_id, 'wc_sc_add_product_details', true );
}
return apply_filters( 'wc_sc_coupon_actions', $actions, array( 'coupon_code' => $coupon_code ) );
}
return array();
}
/**
* Modify cart item data
*
* @param array $cart_item_data The cart item data.
* @param integer $product_id The product id.
* @param integer $variation_id The variation id.
* @param integer $quantity The quantity of product.
* @return array $cart_item_data
*/
public function modify_cart_item_data( $cart_item_data = array(), $product_id = 0, $variation_id = 0, $quantity = 0 ) {
if ( empty( $cart_item_data ) || empty( $product_id ) || empty( $quantity ) ) {
return $cart_item_data;
}
if ( ! empty( $cart_item_data['wc_sc_product_source'] ) ) {
$coupon_code = $cart_item_data['wc_sc_product_source'];
$coupon_actions = $this->get_coupon_actions( $coupon_code );
if ( ! empty( $coupon_actions ) && ! is_scalar( $coupon_actions ) ) {
foreach ( $coupon_actions as $product_data ) {
if ( ! empty( $product_data['product_id'] ) && in_array( absint( $product_data['product_id'] ), array_map( 'absint', array( $product_id, $variation_id ) ), true ) ) {
$discount_amount = ( '' !== $product_data['discount_amount'] ) ? $product_data['discount_amount'] : '';
if ( '' !== $discount_amount ) {
if ( ! empty( $variation_id ) ) {
$product = wc_get_product( $variation_id );
} else {
$product = wc_get_product( $product_id );
}
$product_price = $product->get_price();
$regular_price = $product->get_regular_price();
$discount_type = ( ! empty( $product_data['discount_type'] ) ) ? $product_data['discount_type'] : 'percent';
switch ( $discount_type ) {
case 'flat':
$discount = $this->convert_price( $discount_amount );
break;
case 'percent':
$discount = ( $product_price * $discount_amount ) / 100;
break;
}
$discount = wc_cart_round_discount( min( $product_price, $discount ), 2 );
$discounted_price = $product_price - $discount;
$cart_item_data['data']->set_price( $discounted_price );
$cart_item_data['data']->set_regular_price( $regular_price );
$cart_item_data['data']->set_sale_price( $discounted_price );
}
break;
}
}
}
}
return $cart_item_data;
}
/**
* Modify cart item in WC_Cart::add_to_cart()
*
* @param array $cart_item_data The cart item data as passed by filter 'woocommerce_add_cart_item'.
* @param string $cart_item_key The cart item key.
* @return array $cart_item_data
*/
public function modify_cart_item_data_in_add_to_cart( $cart_item_data = array(), $cart_item_key = '' ) {
if ( ! empty( $cart_item_data['wc_sc_product_source'] ) ) {
$cart_item_data = $this->modify_cart_item_data( $cart_item_data, $cart_item_data['product_id'], $cart_item_data['variation_id'], $cart_item_data['quantity'] );
}
return $cart_item_data;
}
/**
* Modify cart item in session
*
* @param array $session_data The session data.
* @param array $values The cart item.
* @param string $key The cart item key.
* @return array $session_data
*/
public function modify_cart_item_in_session( $session_data = array(), $values = array(), $key = '' ) {
if ( ! empty( $values['wc_sc_product_source'] ) ) {
$session_data['wc_sc_product_source'] = $values['wc_sc_product_source'];
$qty = ( ! empty( $session_data['quantity'] ) ) ? absint( $session_data['quantity'] ) : ( ( ! empty( $values['quantity'] ) ) ? absint( $values['quantity'] ) : 1 );
$session_data = $this->modify_cart_item_data( $session_data, $session_data['product_id'], $session_data['variation_id'], $qty );
}
return $session_data;
}
/**
* Modify cart item quantity
*
* @param string $product_quantity The product quantity.
* @param string $cart_item_key The cart item key.
* @param array $cart_item The cart item.
* @return string $product_quantity
*/
public function modify_cart_item_quantity( $product_quantity = '', $cart_item_key = '', $cart_item = array() ) {
if ( ! empty( $cart_item['wc_sc_product_source'] ) ) {
$product_quantity = sprintf( '%s <input type="hidden" name="cart[%s][qty]" value="%s" />', $cart_item['quantity'], $cart_item_key, $cart_item['quantity'] );
}
return $product_quantity;
}
/**
* Modify cart item price
*
* @param string $product_price The product price.
* @param array $cart_item The cart item.
* @param string $cart_item_key The cart item key.
* @return string $product_price
*/
public function modify_cart_item_price( $product_price = '', $cart_item = array(), $cart_item_key = '' ) {
if ( ( is_array( $cart_item ) && isset( $cart_item['wc_sc_product_source'] ) ) || ( is_object( $cart_item ) && is_callable( array( $cart_item, 'get_meta' ) ) && $cart_item->get_meta( '_wc_sc_product_source' ) ) ) {
if ( wc_price( 0 ) === $product_price ) {
$product_price = apply_filters(
'wc_sc_price_zero_text',
$product_price,
array(
'cart_item' => $cart_item,
'cart_item_key' => $cart_item_key,
)
);
}
}
return $product_price;
}
/**
* Remove products added by the coupon from validation
*
* Since the filter 'woocommerce_coupon_get_items_to_validate' is added in WooCommerce 3.4.0, this function will work only in WC 3.4.0+
* Otherwise, the products added by coupon might get double discounts applied
*
* @param array $items The cart/order items.
* @param WC_Discounts $discounts The discounts object.
* @return mixed $items
*/
public function remove_products_from_validation( $items = array(), $discounts = null ) {
if ( ! empty( $items ) && ! is_scalar( $items ) ) {
foreach ( $items as $index => $item ) {
$coupon_code = '';
if ( is_array( $item->object ) && isset( $item->object['wc_sc_product_source'] ) ) {
$coupon_code = $item->object['wc_sc_product_source'];
} elseif ( is_a( $item->object, 'WC_Order_Item' ) && is_callable( array( $item->object, 'get_meta' ) ) && $item->object->get_meta( '_wc_sc_product_source' ) ) {
$coupon_code = $item->object->get_meta( '_wc_sc_product_source' );
}
if ( ! empty( $coupon_code ) ) {
$item_product_id = ( is_a( $item->product, 'WC_Product' ) && is_callable( array( $item->product, 'get_id' ) ) ) ? $item->product->get_id() : 0;
$coupon_actions = $this->get_coupon_actions( $coupon_code );
if ( ! empty( $coupon_actions ) && ! is_scalar( $coupon_actions ) ) {
foreach ( $coupon_actions as $product_data ) {
if ( ! empty( $product_data['product_id'] ) && absint( $product_data['product_id'] ) === absint( $item_product_id ) ) {
$discount_amount = ( '' !== $product_data['discount_amount'] ) ? $product_data['discount_amount'] : '';
if ( '' !== $discount_amount ) {
unset( $items[ $index ] );
}
}
}
}
}
}
}
return $items;
}
/**
* Apply coupons actions
*
* @param string $coupon_code The coupon code.
* @param WC_Order $order WooCommerce order instance.
*/
public function coupon_action( $coupon_code = '', $order = null ) {
if ( empty( $coupon_code ) ) {
return;
}
if ( $coupon_code instanceof WC_Coupon && is_callable( array( $coupon_code, 'get_code' ) ) ) {
$coupon_code = $coupon_code->get_code();
}
$coupon_actions = $this->get_coupon_actions( $coupon_code );
if ( ! empty( $coupon_actions ) && ! is_scalar( $coupon_actions ) ) {
$product_names = array();
foreach ( $coupon_actions as $coupon_action ) {
if ( empty( $coupon_action['product_id'] ) ) {
continue;
}
$id = absint( $coupon_action['product_id'] );
$product = wc_get_product( $id );
$product_data = $this->get_product_data( $product );
$product_id = ( ! empty( $product_data['product_id'] ) ) ? absint( $product_data['product_id'] ) : 0;
$variation_id = ( ! empty( $product_data['variation_id'] ) ) ? absint( $product_data['variation_id'] ) : 0;
$variation = array();
if ( ! empty( $variation_id ) ) {
$variation = $product->get_variation_attributes();
}
$quantity = absint( $coupon_action['quantity'] );
$cart_item_data = array(
'wc_sc_product_source' => $coupon_code,
);
if ( $order instanceof WC_Abstract_Order ) {
$product_exists = false;
$order_items = $order->get_items();
foreach ( $order_items as $order_item ) {
if ( absint( $order_item->get_product_id() ) === $product_id ) {
$product_exists = true; // Exit if the product is already in the order.
break;
}
}
if ( ! $product_exists ) {
// Add a line item to the order.
$item = new WC_Order_Item_Product();
$discount_amount = ( '' !== $product_data['discount_amount'] ) ? $coupon_action['discount_amount'] : '';
$discount_type = ( ! empty( $coupon_action['discount_type'] ) ) ? $coupon_action['discount_type'] : 'percent';
switch ( $discount_type ) {
case 'flat':
$discount = $this->convert_price( $discount_amount );
break;
case 'percent':
$discount = ( $product->get_price() * $discount_amount ) / 100;
break;
}
// Calculated the discounted amount.
$discounted_price = ( $product->get_price() - $discount ) * $quantity;
$item->set_props(
array(
'name' => $product->get_name(),
'product_id' => $product_id, // ID of the product to add.
'quantity' => $quantity, // Quantity of the product.
'variation_id' => $variation_id, // ID of the product variation (if applicable).
)
);
$item->set_subtotal( $discounted_price );
$item->add_meta_data( '_wc_sc_product_source', $coupon_code );
$item->set_order_id( $order->get_id() );
// Add the item to the order.
$order->add_item( $item );
}
} else {
$cart_item_key = WC()->cart->add_to_cart( $product_id, $quantity, $variation_id, $variation, $cart_item_data );
if ( ! empty( $cart_item_key ) ) {
if ( $this->is_wc_gte_30() ) {
$product_names[] = ( is_object( $product ) && is_callable( array( $product, 'get_name' ) ) ) ? $product->get_name() : '';
} else {
$product_names[] = ( is_object( $product ) && is_callable( array( $product, 'get_title' ) ) ) ? $product->get_title() : '';
}
}
}
}
if ( ! ( $order instanceof WC_Abstract_Order ) && ! empty( $product_names ) ) {
/* translators: 1. Product title */
wc_add_notice( sprintf( __( '%s has been added to your cart!', 'woocommerce-smart-coupons' ), implode( ', ', $product_names ) ) );
}
}
}
/**
* Remove products from cart if the coupon, which added the product, is removed
*
* @param string $coupon_code The coupon code.
*/
public function remove_product_from_cart( $coupon_code = '' ) {
if ( is_admin() ) {
return;
}
if ( ! empty( $coupon_code ) ) {
foreach ( WC()->cart->get_cart() as $cart_item_key => $cart_item ) {
if ( isset( $cart_item['wc_sc_product_source'] ) && $cart_item['wc_sc_product_source'] === $coupon_code ) {
// Action 'woocommerce_before_calculate_totals' is hooked by WooCommerce Subscription while removing coupons in local WooCommerce Cart variable in which we don't need to remove added cart item.
if ( ! doing_action( 'woocommerce_before_calculate_totals' ) ) {
WC()->cart->set_quantity( $cart_item_key, 0 );
}
}
}
}
}
/**
* Remove products added by coupon actions when the coupon is removed from the order.
*/
public function remove_product_from_order() {
$action = ( ! empty( $_POST['action'] ) ) ? wc_clean( wp_unslash( $_POST['action'] ) ) : ''; // phpcs:ignore
$coupon_code = ( ! empty( $_POST['coupon'] ) ) ? wc_clean( wp_unslash( $_POST['coupon'] ) ) : ''; // phpcs:ignore
$order_id = ( ! empty( $_POST['order_id'] ) ) ? wc_clean( wp_unslash( $_POST['order_id'] ) ) : 0; // phpcs:ignore
if ( 'woocommerce_remove_order_coupon' !== $action || empty( $order_id ) || empty( $coupon_code ) ) {
return;
}
$order = wc_get_order( $order_id );
if ( ! $order instanceof WC_Order ) {
return;
}
foreach ( $order->get_items() as $item_id => $item ) {
if ( $item->get_meta( '_wc_sc_product_source', true ) === $coupon_code ) {
$order->remove_item( $item_id );
}
}
$order->calculate_totals(); // Recalculate totals.
$order->save(); // Save the order.
}
/**
* Review cart items
*/
public function review_cart_items() {
$cart = ( is_object( WC() ) && isset( WC()->cart ) ) ? WC()->cart : null;
$applied_coupons = isset( $cart->applied_coupons ) ? (array) $cart->applied_coupons : array();
$products = array();
if ( $cart instanceof WC_Cart && is_callable( array( $cart, 'get_cart' ) ) ) {
foreach ( $cart->get_cart() as $cart_item_key => $cart_item ) {
if ( ! empty( $cart_item['wc_sc_product_source'] ) && ! in_array( $cart_item['wc_sc_product_source'], $applied_coupons, true ) ) {
$cart->set_quantity( $cart_item_key, 0 );
$coupon_code = $cart_item['wc_sc_product_source'];
if ( empty( $products[ $coupon_code ] ) || ! is_array( $products[ $coupon_code ] ) ) {
$products[ $coupon_code ] = array();
}
$products[ $coupon_code ][] = ( is_object( $cart_item['data'] ) && is_callable( array( $cart_item['data'], 'get_name' ) ) ) ? $cart_item['data']->get_name() : '';
$products[ $coupon_code ] = array_filter( $products[ $coupon_code ] );
}
}
}
if ( ! empty( $products ) && ! is_scalar( $products ) ) {
foreach ( $products as $coupon_code => $product_names ) {
/* translators: 1. Product/s 2. Product names 3. is/are 4. Coupons code */
wc_add_notice( sprintf( __( '%1$s %2$s %3$s removed because coupon %4$s is removed.', 'woocommerce-smart-coupons' ), _n( 'Product', 'Products', count( $products[ $coupon_code ] ), 'woocommerce-smart-coupons' ), '<strong>' . implode( ', ', $products[ $coupon_code ] ) . '</strong>', _n( 'is', 'are', count( $products[ $coupon_code ] ), 'woocommerce-smart-coupons' ), '<code>' . $coupon_code . '</code>' ), 'error' );
}
}
}
/**
* Add product source in order item meta
*
* @param mixed $item The item.
* @param string $cart_item_key The cart item key.
* @param array $values The cart item.
* @param WC_Order $order The order.
*/
public function add_product_source_in_order_item_meta( $item = null, $cart_item_key = '', $values = array(), $order = null ) {
if ( isset( $values['wc_sc_product_source'] ) ) {
$item->add_meta_data( '_wc_sc_product_source', $values['wc_sc_product_source'], true );
}
}
/**
* Get product data
*
* @param mixed $product The product object.
* @return array
*/
public function get_product_data( $product = null ) {
if ( empty( $product ) ) {
return array();
}
if ( $this->is_wc_gte_30() ) {
$product_id = ( is_object( $product ) && is_callable( array( $product, 'get_id' ) ) ) ? $product->get_id() : 0;
} else {
$product_id = ( ! empty( $product->id ) ) ? $product->id : 0;
}
$product_type = ( is_object( $product ) && is_callable( array( $product, 'get_type' ) ) ) ? $product->get_type() : '';
if ( 'variation' === $product_type ) {
$variation_id = $product_id;
if ( $this->is_wc_gte_30() ) {
$parent_id = ( is_object( $product ) && is_callable( array( $product, 'get_parent_id' ) ) ) ? $product->get_parent_id() : 0;
$variation_data = wc_get_product_variation_attributes( $variation_id );
} else {
$parent_id = ( is_object( $product ) && is_callable( array( $product, 'get_parent' ) ) ) ? $product->get_parent() : 0;
$variation_data = ( ! empty( $product->variation_data ) ) ? $product->variation_data : array();
}
$product_id = $parent_id;
} else {
$variation_id = 0;
$variation_data = array();
}
$product_data = array(
'product_id' => $product_id,
'variation_id' => $variation_id,
'variation_data' => $variation_data,
);
return apply_filters( 'wc_sc_product_data', $product_data, array( 'product_obj' => $product ) );
}
/**
* Add action's meta in export headers
*
* @param array $headers Existing headers.
* @return array
*/
public function export_headers( $headers = array() ) {
$action_headers = array(
'wc_sc_add_product_details' => __( 'Add product details', 'woocommerce-smart-coupons' ),
);
return array_merge( $headers, $action_headers );
}
/**
* Function to handle coupon meta data during export of existing coupons
*
* @param mixed $meta_value The meta value.
* @param array $args Additional arguments.
* @return string Processed meta value
*/
public function export_coupon_meta_data( $meta_value = '', $args = array() ) {
$index = ( ! empty( $args['index'] ) ) ? $args['index'] : -1;
$meta_keys = ( ! empty( $args['meta_keys'] ) ) ? $args['meta_keys'] : array();
$meta_values = ( ! empty( $args['meta_values'] ) ) ? $args['meta_values'] : array();
if ( $index >= 0 && ! empty( $meta_keys[ $index ] ) && 'wc_sc_add_product_details' === $meta_keys[ $index ] ) {
if ( ! empty( $meta_value ) && is_array( $meta_value ) ) {
$product_details = array();
foreach ( $meta_value as $value ) {
$product_details[] = implode( ',', $value );
}
$meta_value = implode( '|', $product_details );
}
}
return $meta_value;
}
/**
* Post meta defaults for action's meta
*
* @param array $defaults Existing postmeta defaults.
* @return array
*/
public function postmeta_defaults( $defaults = array() ) {
$actions_defaults = array(
'wc_sc_add_product_details' => '',
);
return array_merge( $defaults, $actions_defaults );
}
/**
* Add action's meta with value in coupon meta
*
* @param array $data The row data.
* @param array $post The POST values.
* @return array Modified data
*/
public function generate_coupon_meta( $data = array(), $post = array() ) {
if ( isset( $post['wc_sc_add_product_ids'] ) ) {
if ( $this->is_wc_gte_30() ) {
$product_ids = wc_clean( wp_unslash( $post['wc_sc_add_product_ids'] ) ); // phpcs:ignore
} else {
$product_ids = array_filter( array_map( 'trim', explode( ',', sanitize_text_field( wp_unslash( $post['wc_sc_add_product_ids'] ) ) ) ) ); // phpcs:ignore
}
$add_product_details = array();
if ( ! empty( $product_ids ) && ! is_scalar( $product_ids ) ) {
$quantity = ( isset( $post['wc_sc_add_product_qty'] ) ) ? wc_clean( wp_unslash( $post['wc_sc_add_product_qty'] ) ) : 1;
$discount_amount = ( isset( $post['wc_sc_product_discount_amount'] ) ) ? wc_clean( wp_unslash( $post['wc_sc_product_discount_amount'] ) ) : '';
$discount_type = ( isset( $post['wc_sc_product_discount_type'] ) ) ? wc_clean( wp_unslash( $post['wc_sc_product_discount_type'] ) ) : '';
foreach ( $product_ids as $id ) {
$product_data = array();
$product_data['product_id'] = $id;
$product_data['quantity'] = $quantity;
$product_data['discount_amount'] = $discount_amount;
$product_data['discount_type'] = $discount_type;
$add_product_details[] = implode( ',', $product_data );
}
}
$data['wc_sc_add_product_details'] = implode( '|', $add_product_details );
}
return $data;
}
/**
* Make meta data of SC actions, protected
*
* @param bool $protected Is protected.
* @param string $meta_key The meta key.
* @param string $meta_type The meta type.
* @return bool $protected
*/
public function make_action_meta_protected( $protected, $meta_key, $meta_type ) {
$sc_meta = array(
'wc_sc_add_product_details',
);
if ( in_array( $meta_key, $sc_meta, true ) ) {
return true;
}
return $protected;
}
/**
* Function to copy coupon action meta in newly generated coupon
*
* @param array $args The arguments.
*/
public function copy_coupon_action_meta( $args = array() ) {
$new_coupon_id = ( ! empty( $args['new_coupon_id'] ) ) ? absint( $args['new_coupon_id'] ) : 0;
$coupon = ( ! empty( $args['ref_coupon'] ) ) ? $args['ref_coupon'] : false;
if ( empty( $new_coupon_id ) || empty( $coupon ) ) {
return;
}
$add_product_details = array();
if ( $this->is_wc_gte_30() ) {
$add_product_details = $coupon->get_meta( 'wc_sc_add_product_details' );
} else {
$old_coupon_id = ( ! empty( $coupon->id ) ) ? $coupon->id : 0;
$add_product_details = get_post_meta( $old_coupon_id, 'wc_sc_add_product_details', true );
}
$this->update_post_meta( $new_coupon_id, 'wc_sc_add_product_details', $add_product_details );
}
/**
* Function to validate whether to show the coupon or not
*
* @param boolean $is_show Show or not.
* @param array $args Additional arguments.
* @return boolean
*/
public function show_coupon_with_actions( $is_show = false, $args = array() ) {
$coupon = ( ! empty( $args['coupon'] ) ) ? $args['coupon'] : null;
if ( empty( $coupon ) ) {
return $is_show;
}
if ( $this->is_wc_gte_30() ) {
$coupon_code = ( is_object( $coupon ) && is_callable( array( $coupon, 'get_code' ) ) ) ? $coupon->get_code() : '';
} else {
$coupon_code = ( ! empty( $coupon->code ) ) ? $coupon->code : '';
}
$coupon_actions = $this->get_coupon_actions( $coupon_code );
if ( ! empty( $coupon_actions ) ) {
return true;
}
return $is_show;
}
/**
* Allow auto-generate of coupon with coupon action
*
* @param boolean $is_auto_generate Whether to auto-generate or not.
* @param array $args Additional parameters.
* @return boolean $is_auto_generate
*/
public function auto_generate_coupon_with_actions( $is_auto_generate = false, $args = array() ) {
$coupon = ( ! empty( $args['coupon_obj'] ) && $args['coupon_obj'] instanceof WC_Coupon ) ? $args['coupon_obj'] : false;
$coupon_id = ( ! empty( $args['coupon_id'] ) ) ? $args['coupon_id'] : false;
if ( ! empty( $coupon ) && ! empty( $coupon_id ) ) {
if ( $this->is_wc_gte_30() ) {
$coupon_code = $coupon->get_code();
} else {
$coupon_code = ( ! empty( $coupon->code ) ) ? $coupon->code : '';
}
if ( ! empty( $coupon_code ) ) {
$actions = ( $this->is_callable( $coupon, 'get_meta' ) ) ? $coupon->get_meta( 'wc_sc_add_product_details' ) : get_post_meta( $coupon_id, 'wc_sc_add_product_details', true );
$coupon_actions = apply_filters(
'wc_sc_coupon_actions',
$actions,
array(
'coupon_code' => $coupon_code,
'source' => $this,
)
);
if ( ! empty( $coupon_actions ) ) {
return true;
}
}
}
return $is_auto_generate;
}
/**
* Validate coupon having actions but without an amount
*
* @param boolean $is_valid_coupon_amount Whether the amount is validate or not.
* @param array $args Additional parameters.
* @return boolean
*/
public function validate_coupon_amount( $is_valid_coupon_amount = true, $args = array() ) {
if ( ! $is_valid_coupon_amount ) {
$coupon_amount = ( ! empty( $args['coupon_amount'] ) ) ? $args['coupon_amount'] : 0;
$discount_type = ( ! empty( $args['discount_type'] ) ) ? $args['discount_type'] : '';
$coupon_code = ( ! empty( $args['coupon_code'] ) ) ? $args['coupon_code'] : '';
$coupon_actions = ( ! empty( $coupon_code ) ) ? $this->get_coupon_actions( $coupon_code ) : array();
if ( 'smart_coupon' === $discount_type && $coupon_amount <= 0 && ! empty( $coupon_actions ) ) {
return true;
}
}
return $is_valid_coupon_amount;
}
/**
* Handle coupon actions when the cart is empty
*
* @param boolean $is_hold Whether to hold the coupon in cookie.
* @param array $args Additional arguments.
* @return boolean
*/
public function maybe_run_coupon_actions( $is_hold = true, $args = array() ) {
$cart = ( is_object( WC() ) && isset( WC()->cart ) ) ? WC()->cart : null;
if ( empty( $cart ) || WC()->cart->is_empty() ) {
$coupons_data = ( ! empty( $args['coupons_data'] ) ) ? $args['coupons_data'] : array();
if ( ! empty( $coupons_data ) && ! is_scalar( $coupons_data ) ) {
foreach ( $coupons_data as $coupon_data ) {
$coupon_code = ( ! empty( $coupon_data['coupon-code'] ) ) ? $coupon_data['coupon-code'] : '';
if ( ! empty( $coupon_code ) ) {
$coupon = new WC_Coupon( $coupon_code );
$coupon_actions = $this->get_coupon_actions( $coupon_code );
$coupon_products = ( ! empty( $coupon_actions ) ) ? wp_list_pluck( $coupon_actions, 'product_id' ) : array();
if ( ! empty( $coupon_products ) ) {
if ( $this->is_valid( $coupon ) && ! WC()->cart->has_discount( $coupon_code ) ) {
WC()->cart->add_discount( trim( $coupon_code ) );
$is_hold = false;
}
}
}
}
}
}
return $is_hold;
}
/**
* Stop product update option for action tab products.
*
* @param string $cart_item_key contains the id of the cart item.
* @param int $quantity Current quantity of the item.
* @param int $old_quantity Old quantity of the item.
* @param WC_Cart $cart Cart object.
* @return void
*/
public function stop_cart_item_quantity_update( $cart_item_key = '', $quantity = 1, $old_quantity = 1, $cart = object ) {
if ( empty( $cart_item_key ) || ! is_object( $cart ) || ! is_a( $cart, 'WC_Cart' ) ) {
return;
}
$cart_data = is_callable( array( $cart, 'get_cart' ) ) ? $cart->get_cart() : array();
$cart_item = ( ! empty( $cart_data[ $cart_item_key ] ) ) ? $cart_data[ $cart_item_key ] : array();
if ( empty( $cart_item ) ) {
return;
}
$applied_coupons = is_callable( array( $cart, 'get_applied_coupons' ) ) ? $cart->get_applied_coupons() : array();
if ( ! empty( $cart_item['wc_sc_product_source'] ) && in_array( $cart_item['wc_sc_product_source'], $applied_coupons, true ) ) {
$cart->cart_contents[ $cart_item_key ]['quantity'] = $old_quantity;
}
}
}
}
WC_SC_Coupon_Actions::get_instance();

View File

@@ -0,0 +1,253 @@
<?php
/**
* Smart Coupons category fields in coupons
*
* @author StoreApps
* @since 4.8.0
* @version 1.4.0
*
* @package woocommerce-smart-coupons/includes/
*/
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
if ( ! class_exists( 'WC_SC_Coupon_Categories' ) ) {
/**
* Class for handling Smart Coupons' field in coupons
*/
class WC_SC_Coupon_Categories {
/**
* Variable to hold instance of WC_SC_Coupon_Categories
*
* @var $instance
*/
private static $instance = null;
/**
* Get single instance of WC_SC_Coupon_Categories
*
* @return WC_SC_Coupon_Categories Singleton object of WC_SC_Coupon_Categories
*/
public static function get_instance() {
// Check if instance is already exists.
if ( is_null( self::$instance ) ) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Constructor
*/
private function __construct() {
add_action( 'init', array( $this, 'wc_sc_coupons_add_category' ) );
add_filter( 'parent_file', array( $this, 'wc_sc_make_menu_active' ) );
add_action( 'admin_footer', array( $this, 'add_footer_script' ) );
add_filter( 'manage_edit-sc_coupon_category_columns', array( $this, 'wc_sc_coupon_category_id_column' ) );
add_filter( 'manage_sc_coupon_category_custom_column', array( $this, 'wc_sc_coupon_category_id_column_content' ), 10, 3 );
add_filter( 'manage_edit-sc_coupon_category_sortable_columns', array( $this, 'wc_sc_define_sortable_id_columns' ) );
add_filter( 'manage_shop_coupon_posts_columns', array( $this, 'define_columns' ), 11 );
add_action( 'manage_shop_coupon_posts_custom_column', array( $this, 'render_columns' ), 10, 2 );
}
/**
* Handle call to functions which is not available in this class
*
* @param string $function_name The function name.
* @param array $arguments Array of arguments passed while calling $function_name.
* @return result of function call
*/
public function __call( $function_name, $arguments = array() ) {
global $woocommerce_smart_coupon;
if ( ! is_callable( array( $woocommerce_smart_coupon, $function_name ) ) ) {
return;
}
if ( ! empty( $arguments ) ) {
return call_user_func_array( array( $woocommerce_smart_coupon, $function_name ), $arguments );
} else {
return call_user_func( array( $woocommerce_smart_coupon, $function_name ) );
}
}
/**
* Register custom taxonomy called sc_coupon_category.
*/
public function wc_sc_coupons_add_category() {
$labels = array(
'name' => __( 'Coupon categories', 'woocommerce-smart-coupons' ),
'singular_name' => __( 'Category', 'woocommerce-smart-coupons' ),
'menu_name' => _x( 'Categories', 'Admin menu name', 'woocommerce-smart-coupons' ),
'search_items' => __( 'Search coupon categories', 'woocommerce-smart-coupons' ),
'all_items' => __( 'All coupon categories', 'woocommerce-smart-coupons' ),
'parent_item' => __( 'Parent coupon category', 'woocommerce-smart-coupons' ),
'parent_item_colon' => __( 'Parent coupon category:', 'woocommerce-smart-coupons' ),
'edit_item' => __( 'Edit coupon category', 'woocommerce-smart-coupons' ),
'update_item' => __( 'Update coupon category', 'woocommerce-smart-coupons' ),
'add_new_item' => __( 'Add new coupon category', 'woocommerce-smart-coupons' ),
'new_item_name' => __( 'New coupon category name', 'woocommerce-smart-coupons' ),
'not_found' => __( 'No coupon categories found', 'woocommerce-smart-coupons' ),
);
register_taxonomy(
'sc_coupon_category', // Taxonomy name.
array( 'shop_coupon' ), // object for which the taxonomy is created.
array(
'labels' => $labels,
'description' => __( 'Manage coupon categories', 'woocommerce-smart-coupons' ),
'public' => true,
'hierarchical' => true,
'show_ui' => true,
'show_in_menu' => false,
'show_in_rest' => true,
'show_admin_column' => true,
'rewrite' => array( 'slug' => 'sc_coupon_category' ),
'query_var' => true,
)
);
register_taxonomy_for_object_type( 'sc_coupon_category', 'shop_coupon' );
}
/**
* Function to render coupon category column on coupons dashboard.
*
* @param int $post_id The coupon ID.
* @param WC_Coupon $coupon The coupon object.
*/
public function render_coupon_category_column( $post_id = 0, $coupon = null ) {
$terms = get_the_terms( $post_id, 'sc_coupon_category' );
if ( ! empty( $terms ) ) {
foreach ( $terms as $term ) {
$content[] = '<a href="' . esc_url( admin_url( 'edit.php?sc_coupon_category=' . $term->slug . '&post_type=shop_coupon' ) ) . '">' . esc_html( $term->name ) . '</a>';
}
echo join( ', ', $content ); // phpcs:ignore
} else {
echo '<span class="na">&ndash;</span>';
}
}
/**
* Function to set woocommerce menu active
*
* @param string $parent_file file reference for menu.
*/
public function wc_sc_make_menu_active( $parent_file ) {
global $current_screen;
$taxonomy = $current_screen->taxonomy;
if ( 'sc_coupon_category' === $taxonomy ) {
if ( $this->is_wc_gte_44() ) {
$parent_file = 'woocommerce-marketing';
} else {
$parent_file = 'woocommerce';
}
}
return $parent_file;
}
/**
* Function to add custom script in admin footer.
*/
public function add_footer_script() {
global $pagenow, $post;
$get_post_type = ( ! empty( $post->ID ) ) ? $this->get_post_type( $post->ID ) : ( ( ! empty( $_GET['post_type'] ) ) ? wc_clean( wp_unslash( $_GET['post_type'] ) ) : '' ); // phpcs:ignore
if ( 'shop_coupon' === $get_post_type ) {
if ( 'post.php' === $pagenow || 'post-new.php' === $pagenow ) {
$manage_category_string = __( 'Manage coupon categories', 'woocommerce-smart-coupons' );
?>
<script type="text/javascript">
jQuery(function() {
jQuery('#sc_coupon_category-tabs').before('<div class="sc-manage-category"><a target="_blank" href="<?php echo esc_url( admin_url( 'edit-tags.php?taxonomy=sc_coupon_category&post_type=shop_coupon' ) ); ?> "><?php echo esc_html( $manage_category_string ); ?></a></div>');
});
</script>
<?php
}
}
}
/**
* Function for add id column in coupon category taxonomy page.
*
* @param array $columns - Existing headers.
* @return array
*/
public function wc_sc_coupon_category_id_column( $columns = array() ) {
if ( ! empty( $columns ) ) {
return array_slice( $columns, 0, 1, true ) + array( 'id' => 'ID' ) + array_slice( $columns, 1, count( $columns ) - 1, true );
}
return $columns;
}
/**
* Function for add content to ID column of coupon category taxonomy page.
*
* @param string $content - column content.
* @param string $column_name - column_name.
* @param string $term_id - id.
* @return string
*/
public function wc_sc_coupon_category_id_column_content( $content = '', $column_name = '', $term_id = 0 ) {
return $term_id;
}
/**
* Function for change ID column to sortable.
*
* @param array $columns - Existing columns.
* @return array
*/
public function wc_sc_define_sortable_id_columns( $columns = array() ) {
$columns['id'] = 'id';
return $columns;
}
/**
* Define which columns to show on this screen.
*
* @param array $columns Existing columns.
* @return array
*/
public function define_columns( $columns = array() ) {
if ( ! is_array( $columns ) || empty( $columns ) ) {
$columns = array();
}
$columns['wc_sc_coupon_category'] = __( 'Coupon categories', 'woocommerce-smart-coupons' );
return $columns;
}
/**
* Render individual columns.
*
* @param string $column Column ID to render.
* @param int $post_id Post ID being shown.
*/
public function render_columns( $column = '', $post_id = 0 ) {
if ( empty( $post_id ) || empty( $column ) || 'wc_sc_coupon_category' !== $column ) {
return;
}
$coupon = new WC_Coupon( $post_id );
$this->render_coupon_category_column( $post_id, $coupon );
}
}
}
WC_SC_Coupon_Categories::get_instance();

View File

@@ -0,0 +1,200 @@
<?php
/**
* Handle coupon columns
*
* @author StoreApps
* @since 4.5.2
* @version 1.3.1
*
* @package woocommerce-smart-coupons/includes/
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
if ( ! class_exists( 'WC_SC_Coupon_Columns' ) ) {
/**
* Class for handling coupon columns
*/
class WC_SC_Coupon_Columns {
/**
* Variable to hold instance of WC_SC_Coupon_Columns
*
* @var $instance
*/
private static $instance = null;
/**
* Post type.
*
* @var string
*/
protected $list_table_type = 'shop_coupon';
/**
* Object being shown on the row.
*
* @var object|null
*/
protected $object = null;
/**
* Constructor
*/
private function __construct() {
add_filter( 'manage_' . $this->list_table_type . '_posts_columns', array( $this, 'define_columns' ), 11 );
add_action( 'manage_' . $this->list_table_type . '_posts_custom_column', array( $this, 'render_columns' ), 10, 2 );
}
/**
* Get single instance of WC_SC_Coupon_Columns
*
* @return WC_SC_Coupon_Columns Singleton object of WC_SC_Coupon_Columns
*/
public static function get_instance() {
// Check if instance is already exists.
if ( is_null( self::$instance ) ) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Handle call to functions which is not available in this class
*
* @param string $function_name The function name.
* @param array $arguments Array of arguments passed while calling $function_name.
* @return result of function call
*/
public function __call( $function_name, $arguments = array() ) {
global $woocommerce_smart_coupon;
if ( ! is_callable( array( $woocommerce_smart_coupon, $function_name ) ) ) {
return;
}
if ( ! empty( $arguments ) ) {
return call_user_func_array( array( $woocommerce_smart_coupon, $function_name ), $arguments );
} else {
return call_user_func( array( $woocommerce_smart_coupon, $function_name ) );
}
}
/**
* Pre-fetch any data for the row each column has access to it.
*
* @param int $post_id Post ID being shown.
*/
protected function prepare_row_data( $post_id = 0 ) {
if ( empty( $post_id ) ) {
return;
}
$coupon_id = 0;
if ( ! empty( $this->object ) ) {
$coupon = $this->object;
if ( $this->is_wc_gte_30() ) {
$coupon_id = ( is_object( $coupon ) && is_callable( array( $coupon, 'get_id' ) ) ) ? $coupon->get_id() : 0;
} else {
$coupon_id = ( ! empty( $coupon->id ) ) ? $coupon->id : 0;
}
}
if ( empty( $this->object ) || $coupon_id !== $post_id ) {
$this->object = new WC_Coupon( $post_id );
}
}
/**
* Define which columns to show on this screen.
*
* @param array $columns Existing columns.
* @return array
*/
public function define_columns( $columns = array() ) {
if ( ! is_array( $columns ) || empty( $columns ) ) {
$columns = array();
}
$columns['wc_sc_view_orders'] = __( 'Used in orders', 'woocommerce-smart-coupons' );
return $columns;
}
/**
* Render individual columns.
*
* @param string $column Column ID to render.
* @param int $post_id Post ID being shown.
*/
public function render_columns( $column = '', $post_id = 0 ) {
$this->prepare_row_data( $post_id );
if ( ! $this->object ) {
return;
}
if ( ! empty( $column ) ) {
switch ( $column ) {
case 'wc_sc_view_orders':
$this->render_view_orders_column( $post_id, $this->object );
break;
}
}
}
/**
* Render column: View orders.
*
* @param integer $coupon_id The coupon id.
* @param WC_Coupon $coupon The coupon object.
*/
public function render_view_orders_column( $coupon_id = 0, $coupon = null ) {
if ( $this->is_wc_gte_30() ) {
$usage_count = ( ! empty( $coupon ) && is_callable( array( $coupon, 'get_usage_count' ) ) ) ? $coupon->get_usage_count() : 0;
} else {
$usage_count = ( ! empty( $coupon->usage_count ) ) ? $coupon->usage_count : 0;
}
$column_content = '';
if ( ! empty( $usage_count ) ) {
if ( $this->is_wc_gte_30() ) {
$coupon_code = ( ! empty( $coupon ) && is_callable( array( $coupon, 'get_code' ) ) ) ? $coupon->get_code() : '';
} else {
$coupon_code = ( ! empty( $coupon->code ) ) ? $coupon->code : '';
}
$coupon_usage_url = add_query_arg(
array(
's' => $coupon_code,
'post_status' => 'all',
'post_type' => 'shop_order',
),
admin_url( 'edit.php' )
);
$column_content = sprintf( '<a href="%s" target="_blank"><span class="dashicons dashicons-external"></span></a>', esc_url( $coupon_usage_url ) );
} else {
$column_content = '&ndash;';
}
$column_content = apply_filters( 'wc_sc_view_orders_column_content', $column_content, array( 'coupon' => $coupon ) );
echo wp_kses_post( $column_content );
}
}
}
WC_SC_Coupon_Columns::get_instance();

View File

@@ -0,0 +1,528 @@
<?php
/**
* Class to handle feature Coupon Message
*
* @author Ratnakar
* @category Admin
* @package wocommerce-smart-coupons/includes
* @version 2.3.0
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
if ( ! class_exists( 'WC_SC_Coupon_Message' ) ) {
/**
* Class WC_SC_Coupon_Message
*/
class WC_SC_Coupon_Message {
/**
* Variable to hold instance of this class
*
* @var $instance
*/
private static $instance = null;
/**
* Constructor
*
* @since 1.0
*/
private function __construct() {
add_action( 'wc_smart_coupons_actions', array( $this, 'wc_coupon_message_options' ), 10, 2 );
add_action( 'woocommerce_coupon_options_save', array( $this, 'wc_process_coupon_message_meta' ), 10, 2 );
add_action( 'wp_ajax_get_wc_coupon_message', array( $this, 'get_wc_coupon_message' ) );
add_action( 'wp_ajax_nopriv_get_wc_coupon_message', array( $this, 'get_wc_coupon_message' ) );
add_action( 'woocommerce_before_cart_table', array( $this, 'wc_coupon_message_display' ) );
add_action( 'woocommerce_checkout_before_customer_details', array( $this, 'wc_coupon_message_display' ) );
add_action( 'woocommerce_email_after_order_table', array( $this, 'wc_add_coupons_message_in_email' ), null, 3 );
add_filter( 'wc_smart_coupons_export_headers', array( $this, 'export_headers' ) );
add_filter( 'smart_coupons_parser_postmeta_defaults', array( $this, 'postmeta_defaults' ) );
add_filter( 'sc_generate_coupon_meta', array( $this, 'generate_coupon_meta' ), 10, 2 );
add_filter( 'is_protected_meta', array( $this, 'make_action_meta_protected' ), 10, 3 );
add_action( 'wc_sc_new_coupon_generated', array( $this, 'copy_coupon_action_meta' ) );
}
/**
* Get single instance of this class
*
* @return this class Singleton object of this class
*/
public static function get_instance() {
// Check if instance is already exists.
if ( is_null( self::$instance ) ) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Handle call to functions which is not available in this class
*
* @param string $function_name The function name.
* @param array $arguments Array of arguments passed while calling $function_name.
* @return result of function call
*/
public function __call( $function_name, $arguments = array() ) {
global $woocommerce_smart_coupon;
if ( ! is_callable( array( $woocommerce_smart_coupon, $function_name ) ) ) {
return;
}
if ( ! empty( $arguments ) ) {
return call_user_func_array( array( $woocommerce_smart_coupon, $function_name ), $arguments );
} else {
return call_user_func( array( $woocommerce_smart_coupon, $function_name ) );
}
}
/**
* Function to add additional fields for coupon
*
* @param integer $coupon_id The coupon id.
* @param WC_Coupon $coupon The coupon object.
* @since 1.0
*/
public function wc_coupon_message_options( $coupon_id = 0, $coupon = null ) {
global $post;
?>
<style type="text/css">
#wc_coupon_message_options #wc_coupon_message_ifr {
height: 100% !important;
}
#wp-wc_coupon_message-wrap {
display: inline-block;
width: 70%;
margin: -3em 0 0 12.5em;
}
.wp_editor_coupon_message {
width: 100%;
}
</style>
<?php
$editor_args = array(
'textarea_name' => 'wc_coupon_message',
'textarea_rows' => 10,
'editor_class' => 'wp_editor_coupon_message',
'media_buttons' => true,
'tinymce' => true,
);
echo '<div id="wc_coupon_message_options" class="options_group smart-coupons-field">';
?>
<p class="form-field wc_coupon_message_row">
<label for="wc_coupon_message"><?php echo esc_html__( 'Display message', 'woocommerce-smart-coupons' ); ?></label>
<?php $wc_coupon_message = $this->get_post_meta( $post->ID, 'wc_coupon_message', true ); ?>
<?php wp_editor( $wc_coupon_message, 'wc_coupon_message', $editor_args ); ?>
</p>
<?php
woocommerce_wp_checkbox(
array(
'id' => 'wc_email_message',
'label' => __( 'Email message?', 'woocommerce-smart-coupons' ),
'description' => __(
'Check this box to include above message in order confirmation email',
'woocommerce-smart-coupons'
),
)
);
echo '</div>';
}
/**
* Function to save coupon plus data in coupon's meta
*
* @since 1.0
*
* @param integer $post_id Coupon's id.
* @param WC_Coupon $coupon Current coupon object.
*/
public function wc_process_coupon_message_meta( $post_id = 0, $coupon = null ) {
if ( empty( $post_id ) ) {
return;
}
$coupon = new WC_Coupon( $coupon );
$allowed_html = wp_kses_allowed_html( 'post' );
$additional_allowed_html = array(
'style' => array(),
);
$additional_allowed_html = apply_filters( 'wc_sc_kses_allowed_html_for_coupon_message', $additional_allowed_html, array( 'source' => $this ) );
if ( ! empty( $additional_allowed_html ) ) {
foreach ( $additional_allowed_html as $tag => $attributes ) {
if ( ! empty( $attributes ) && array_key_exists( $tag, $allowed_html ) ) {
$allowed_html[ $tag ] = array_merge( $allowed_html[ $tag ], $attributes );
} else {
$allowed_html[ $tag ] = $attributes;
}
}
}
if ( $this->is_callable( $coupon, 'update_meta_data' ) && $this->is_callable( $coupon, 'save' ) ) {
if ( isset( $_POST['wc_coupon_message'] ) ) { // phpcs:ignore
$coupon->update_meta_data( 'wc_coupon_message', wp_kses( wp_unslash( $_POST['wc_coupon_message'] ), $allowed_html ) ); // phpcs:ignore
}
if ( isset( $_POST['wc_email_message'] ) ) { // phpcs:ignore
$coupon->update_meta_data( 'wc_email_message', wc_clean( wp_unslash( $_POST['wc_email_message'] ) ) ); // phpcs:ignore
} else {
$coupon->update_meta_data( 'wc_email_message', 'no' );
}
$coupon->save();
} else {
if ( isset( $_POST['wc_coupon_message'] ) ) { // phpcs:ignore
update_post_meta( $post_id, 'wc_coupon_message', wp_kses( wp_unslash( $_POST['wc_coupon_message'] ), $allowed_html ) ); // phpcs:ignore
}
if ( isset( $_POST['wc_email_message'] ) ) { // phpcs:ignore
update_post_meta( $post_id, 'wc_email_message', wc_clean( wp_unslash( $_POST['wc_email_message'] ) ) ); // phpcs:ignore
} else {
update_post_meta( $post_id, 'wc_email_message', 'no' );
}
}
}
/**
* Function to print coupon message
*
* @param array $applied_coupons Applied coupons.
*/
public function print_coupon_message( $applied_coupons = array() ) {
if ( empty( $applied_coupons ) ) {
echo '<div class="no_wc_coupon_message"></div>';
return;
}
foreach ( $applied_coupons as $coupon_code ) {
$coupon = new WC_Coupon( $coupon_code );
if ( ! $this->is_valid( $coupon ) ) {
continue;
}
if ( $this->is_wc_gte_30() ) {
$coupon_id = ( ! empty( $coupon ) && is_callable( array( $coupon, 'get_id' ) ) ) ? $coupon->get_id() : 0;
} else {
$coupon_id = ( ! empty( $coupon->id ) ) ? $coupon->id : 0;
}
$wc_coupon_message = ( $this->is_callable( $coupon, 'get_meta' ) ) ? $coupon->get_meta( 'wc_coupon_message' ) : get_post_meta( $coupon_id, 'wc_coupon_message', true );
if ( empty( $wc_coupon_message ) ) {
continue;
}
?>
<div id="wc_coupon_message_<?php echo esc_attr( $coupon_id ); ?>" class="wc_coupon_message_container">
<div class="wc_coupon_message_body">
<?php
$is_filter_content = apply_filters(
'wc_sc_is_filter_content_coupon_message',
true,
array(
'source' => $this,
'called_by' => current_filter(),
'coupon_object' => $coupon,
)
);
if ( true === $is_filter_content ) {
$wc_coupon_message = apply_filters( 'the_content', $wc_coupon_message );
}
?>
<?php echo wp_kses_post( $wc_coupon_message ); // phpcs:ignore ?>
</div>
</div>
<?php
}
}
/**
* Function to validate applied coupon's additional field which comes from this plugin
*
* @since 1.0
*/
public function wc_coupon_message_display() {
if ( ! is_object( WC() ) || ! is_object( WC()->cart ) || WC()->cart->is_empty() ) {
return;
}
$applied_coupons = WC()->cart->get_applied_coupons();
?>
<div class="wc_coupon_message_wrap" style="padding: 10px 0 10px;">
<?php $this->print_coupon_message( $applied_coupons ); ?>
</div>
<?php
$js = '';
$cart_page_id = absint( get_option( 'woocommerce_cart_page_id' ) );
$checkout_page_id = absint( get_option( 'woocommerce_checkout_page_id' ) );
if ( has_block( 'woocommerce-smart-coupons/available-coupons', $cart_page_id ) || has_block( 'woocommerce-smart-coupons/available-coupons', $checkout_page_id ) ) { // Code to handle apply coupon via blocks.
$js = "
// Check if sc_coupon_message_ajax is undefined
if (typeof sc_coupon_message_ajax === 'undefined') {
var sc_coupon_message_ajax = null;
}
// Define the event listener
function handleCouponMessage() {
clearTimeout(sc_coupon_message_ajax);
sc_coupon_message_ajax = setTimeout(function () {
var xhr = new XMLHttpRequest();
xhr.open('POST', '" . admin_url( 'admin-ajax.php' ) . "', true);
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xhr.onreadystatechange = function () {
if (xhr.readyState === 4 && xhr.status === 200) {
var response = xhr.responseText;
var wc_coupon_message_wrap = document.querySelector('.wc_coupon_message_wrap');
wc_coupon_message_wrap.innerHTML = '';
if (response !== undefined && response !== '') {
wc_coupon_message_wrap.innerHTML = response;
}
}
};
let data = {
action: 'get_wc_coupon_message',
security: '" . wp_create_nonce( 'wc_coupon_message' ) . "'
};
let formData = '';
for (let key in data) {
formData += key + '=' + data[key] + '&';
}
formData = formData.slice(0, -1); // Remove the trailing '&'
xhr.send(formData);
}, 200);
}
// Add event listeners
document.addEventListener('applied_coupon', handleCouponMessage);
document.addEventListener('removed_coupon', handleCouponMessage);
document.addEventListener('updated_checkout', handleCouponMessage);
";
} else {
if ( is_cart() || is_checkout() ) {
$js = "
if (typeof sc_coupon_message_ajax === 'undefined') {
var sc_coupon_message_ajax = null;
}
jQuery('body').on('applied_coupon removed_coupon updated_checkout', function(){
clearTimeout( sc_coupon_message_ajax );
sc_coupon_message_ajax = setTimeout(function(){
jQuery.ajax({
url: '" . admin_url( 'admin-ajax.php' ) . "',
type: 'POST',
dataType: 'html',
data: {
action: 'get_wc_coupon_message',
security: '" . wp_create_nonce( 'wc_coupon_message' ) . "'
},
success: function( response ) {
jQuery('.wc_coupon_message_wrap').html('');
if ( response != undefined && response != '' ) {
jQuery('.wc_coupon_message_wrap').html( response );
}
}
});
}, 200);
});
";
}
}
if ( ! empty( $js ) ) {
wc_enqueue_js( $js );
}
}
/**
* Function to get coupon messages via ajax
*/
public function get_wc_coupon_message() {
check_ajax_referer( 'wc_coupon_message', 'security' );
$applied_coupons = WC()->cart->get_applied_coupons();
$this->print_coupon_message( $applied_coupons );
die();
}
/**
* Function to add coupon's message in email
*
* @since 1.0
*
* @param WC_Order $order Order's object.
* @param boolean $bool Not used in this function.
* @param boolean $plain_text Not used in this function.
*/
public function wc_add_coupons_message_in_email( $order = null, $bool = false, $plain_text = false ) {
$used_coupons = $this->get_coupon_codes( $order );
if ( count( $used_coupons ) <= 0 ) {
return;
}
$show_coupon_message_title = false;
$coupon_messages = '';
foreach ( $used_coupons as $coupon_code ) {
$coupon = new WC_Coupon( $coupon_code );
if ( $this->is_wc_gte_30() ) {
$coupon_id = ( ! empty( $coupon ) && is_callable( array( $coupon, 'get_id' ) ) ) ? $coupon->get_id() : 0;
} else {
$coupon_id = ( ! empty( $coupon->id ) ) ? $coupon->id : 0;
}
$is_callable_coupon_get_meta = $this->is_callable( $coupon, 'get_meta' );
if ( true === $is_callable_coupon_get_meta ) {
$coupon_message = $coupon->get_meta( 'wc_coupon_message' );
$include_in_email = $coupon->get_meta( 'wc_email_message' );
} else {
$coupon_message = get_post_meta( $coupon_id, 'wc_coupon_message', true );
$include_in_email = get_post_meta( $coupon_id, 'wc_email_message', true );
}
if ( ! empty( $coupon_message ) && 'yes' === $include_in_email ) {
$is_filter_content = apply_filters(
'wc_sc_is_filter_content_coupon_message',
true,
array(
'source' => $this,
'called_by' => current_filter(),
'coupon_object' => $coupon,
'order_object' => $order,
)
);
if ( true === $is_filter_content ) {
$coupon_messages .= apply_filters( 'the_content', $coupon_message );
} else {
$coupon_messages .= $coupon_message;
}
$show_coupon_message_title = true;
}
}
if ( $show_coupon_message_title ) {
?>
<h2><?php echo esc_html__( 'Coupon Message', 'woocommerce-smart-coupons' ); ?></h2>
<?php
echo '<div class="wc_coupon_message_wrap" style="padding: 10px 0 10px;">';
echo wp_kses_post( $coupon_messages ); // phpcs:ignore
echo '</div>';
}
}
/**
* Add meta in export headers
*
* @param array $headers Existing headers.
* @return array
*/
public function export_headers( $headers = array() ) {
$cm_headers = array(
'wc_coupon_message' => __( 'Coupon Message', 'woocommerce-smart-coupons' ),
'wc_email_message' => __( 'Is Email Coupon Message', 'woocommerce-smart-coupons' ),
);
return array_merge( $headers, $cm_headers );
}
/**
* Post meta defaults for CM's meta
*
* @param array $defaults Existing postmeta defaults.
* @return array
*/
public function postmeta_defaults( $defaults = array() ) {
$cm_defaults = array(
'wc_coupon_message' => '',
'wc_email_message' => '',
);
return array_merge( $defaults, $cm_defaults );
}
/**
* Add CM's meta with value in coupon meta
*
* @param array $data The row data.
* @param array $post The POST values.
* @return array Modified data
*/
public function generate_coupon_meta( $data = array(), $post = array() ) {
$data['wc_coupon_message'] = ( ! empty( $post['wc_coupon_message'] ) ) ? wp_kses_post( $post['wc_coupon_message'] ) : '';
$data['wc_email_message'] = ( ! empty( $post['wc_email_message'] ) ) ? wc_clean( wp_unslash( $post['wc_email_message'] ) ) : 'no';
return $data;
}
/**
* Make meta data of SC CM, protected
*
* @param bool $protected Is protected.
* @param string $meta_key The meta key.
* @param string $meta_type The meta type.
* @return bool $protected
*/
public function make_action_meta_protected( $protected, $meta_key, $meta_type ) {
$sc_meta = array(
'wc_coupon_message' => '',
'wc_email_message' => '',
);
if ( in_array( $meta_key, $sc_meta, true ) ) {
return true;
}
return $protected;
}
/**
* Function to copy CM meta in newly generated coupon
*
* @param array $args The arguments.
*/
public function copy_coupon_action_meta( $args = array() ) {
$new_coupon_id = ( ! empty( $args['new_coupon_id'] ) ) ? absint( $args['new_coupon_id'] ) : 0;
$coupon = ( ! empty( $args['ref_coupon'] ) ) ? $args['ref_coupon'] : false;
if ( empty( $new_coupon_id ) || empty( $coupon ) ) {
return;
}
$new_coupon = new WC_Coupon( $new_coupon_id );
if ( $this->is_callable( $new_coupon, 'get_meta' ) && $this->is_callable( $new_coupon, 'update_meta_data' ) && $this->is_callable( $new_coupon, 'save' ) ) {
$coupon_message = $coupon->get_meta( 'wc_coupon_message' );
$email_message = $coupon->get_meta( 'wc_email_message' );
$new_coupon->update_meta_data( 'wc_coupon_message', wp_filter_post_kses( $coupon_message ) );
$new_coupon->update_meta_data( 'wc_email_message', $email_message );
} else {
$old_coupon_id = ( ! empty( $coupon->id ) ) ? $coupon->id : 0;
$coupon_message = get_post_meta( $old_coupon_id, 'wc_coupon_message', true );
$email_message = get_post_meta( $old_coupon_id, 'wc_email_message', true );
update_post_meta( $new_coupon_id, 'wc_coupon_message', wp_filter_post_kses( $coupon_message ) );
update_post_meta( $new_coupon_id, 'wc_email_message', $email_message );
}
}
}
}
WC_SC_Coupon_Message::get_instance();

View File

@@ -0,0 +1,454 @@
<?php
/**
* Coupon parser during import & export
*
* @author StoreApps
* @since 3.3.0
* @version 1.5.0
*
* @package woocommerce-smart-coupons/includes/
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
if ( ! class_exists( 'WC_SC_Coupon_Parser' ) ) {
/**
* Class to parse values for WC_Coupon for importing
*/
class WC_SC_Coupon_Parser {
/**
* The post type
*
* @var string $post_type
*/
public $post_type;
/**
* Reserved fields
*
* @var array $reserved_fields
*/
public $reserved_fields; // Fields we map/handle (not custom fields).
/**
* POst defaults
*
* @var array $post_defaults
*/
public $post_defaults; // Default post data.
/**
* Postmeta defaults
*
* @var array $postmeta_defaults
*/
public $postmeta_defaults; // default post meta.
/**
* Term defaults
*
* @var array $term_defaults
*/
public $term_defaults; // default term data.
/**
* Row number
*
* @var int $row
*/
public $row;
/**
* Constructor
*
* @param string $post_type The post type.
*/
public function __construct( $post_type = 'shop_coupon' ) {
$this->post_type = $post_type;
$this->reserved_fields = array(
'id',
'post_id',
'post_type',
'menu_order',
'postmeta',
'post_status',
'post_title',
'post_name',
'comment_status',
'post_date',
'post_date_gmt',
'post_content',
'post_excerpt',
'post_parent',
'post_password',
'discount_type',
'coupon_amount',
'free_shipping',
'expiry_date',
'minimum_amount',
'maximum_amount',
'individual_use',
'exclude_sale_items',
'product_ids',
'exclude_product_ids',
'product_categories',
'exclude_product_categories',
'customer_email',
'usage_limit',
'usage_limit_per_user',
'limit_usage_to_x_items',
'usage_count',
'_used_by',
);
$this->post_defaults = array(
'post_type' => $this->post_type,
'menu_order' => '',
'postmeta' => array(),
'post_status' => 'publish',
'post_title' => '',
'post_name' => '',
'comment_status' => 'closed',
'post_date' => '',
'post_date_gmt' => '',
'post_content' => '',
'post_excerpt' => '',
'post_parent' => 0,
'post_password' => '',
'post_author' => get_current_user_id(),
);
$this->postmeta_defaults = apply_filters(
'smart_coupons_parser_postmeta_defaults',
array(
'discount_type' => 'fixed_cart',
'coupon_amount' => '',
'free_shipping' => '',
'expiry_date' => '',
'sc_coupon_validity' => '',
'validity_suffix' => '',
'auto_generate_coupon' => '',
'coupon_title_prefix' => '',
'coupon_title_suffix' => '',
'is_pick_price_of_product' => '',
'minimum_amount' => '',
'maximum_amount' => '',
'individual_use' => '',
'exclude_sale_items' => '',
'product_ids' => '',
'exclude_product_ids' => '',
'product_categories' => '',
'exclude_product_categories' => '',
'customer_email' => '',
'sc_disable_email_restriction' => '',
'usage_limit' => '',
'usage_limit_per_user' => '',
'limit_usage_to_x_items' => '',
'sc_is_visible_storewide' => '',
'usage_count' => '',
'_used_by' => '',
'sc_restrict_to_new_user' => '',
'wc_sc_max_discount' => '',
)
);
$this->term_defaults = array(
'sc_coupon_category' => '',
);
}
/**
* Handle call to functions which is not available in this class
*
* @param string $function_name The function name.
* @param array $arguments Array of arguments passed while calling $function_name.
* @return result of function call
*/
public function __call( $function_name, $arguments = array() ) {
global $woocommerce_smart_coupon;
if ( ! is_callable( array( $woocommerce_smart_coupon, $function_name ) ) ) {
return;
}
if ( ! empty( $arguments ) ) {
return call_user_func_array( array( $woocommerce_smart_coupon, $function_name ), $arguments );
} else {
return call_user_func( array( $woocommerce_smart_coupon, $function_name ) );
}
}
/**
* Format data passed from CSV
*
* @param array $data The data to format.
* @param string $enc encoding The encoding.
*/
public function format_data_from_csv( $data, $enc ) {
return ( 'UTF-8' === $enc ) ? $data : utf8_encode( $data );
}
/**
* Parse data
*
* @param string $file Imported file.
* @return array parsed data with headers
*/
public function parse_data( $file ) {
// Set locale.
$enc = $this->mb_detect_encoding( $file, 'UTF-8, ISO-8859-1', true );
if ( $enc ) {
setlocale( LC_ALL, 'en_US.' . $enc );
}
if ( ! $this->is_php_gte( '8.0.0' ) ) {
ini_set( 'auto_detect_line_endings', true ); // phpcs:ignore
}
$parsed_data = array();
$handle = fopen( $file, 'r' ); // phpcs:ignore
// Put all CSV data into an associative array.
if ( false !== $handle ) {
$header = fgetcsv( $handle, 0 );
while ( false !== ( $postmeta = fgetcsv( $handle, 0 ) ) ) { // phpcs:ignore
$row = array();
foreach ( $header as $key => $heading ) {
$s_heading = strtolower( $heading );
$row[ $s_heading ] = ( isset( $postmeta[ $key ] ) ) ? $this->format_data_from_csv( stripslashes( $postmeta[ $key ] ), $enc ) : '';
$raw_headers[ $s_heading ] = $heading;
}
$parsed_data[] = $row;
unset( $postmeta, $row );
}
fclose( $handle ); // phpcs:ignore
}
return array( $parsed_data, $raw_headers );
}
/**
* Parse data one row at a time
*
* @param boolean $file_handler CSV file handler.
* @param array $header CSV header meta column name.
* @param integer $file_position file pointer posistion to read from.
* @param string $encoding Character encoding.
* @return array $result parsed data with current file pointer position
*/
public function parse_data_by_row( $file_handler = false, $header = array(), $file_position = 0, $encoding = '' ) {
$parsed_csv_data = array();
$reading_completed = false;
if ( false !== $file_handler ) {
if ( $file_position > 0 ) {
fseek( $file_handler, (int) $file_position );
}
if ( false !== ( $postmeta = fgetcsv( $file_handler, 0 ) ) ) { // phpcs:ignore
$row = array();
foreach ( $header as $key => $heading ) {
$s_heading = strtolower( $heading );
// Put all CSV data into an associative array by row.
$row[ $s_heading ] = ( isset( $postmeta[ $key ] ) ) ? $this->format_data_from_csv( stripslashes( $postmeta[ $key ] ), $encoding ) : '';
}
$parsed_csv_data = $row;
unset( $postmeta, $row );
} else {
$reading_completed = true;
}
$file_position = ftell( $file_handler );
}
$result = array(
'parsed_csv_data' => $parsed_csv_data,
'file_position' => $file_position,
'reading_completed' => $reading_completed,
);
return $result;
}
/**
* Parse coupon
*
* @param array $item The imported item.
* @return array $coupon
*/
public function parse_coupon( $item ) {
global $wc_csv_coupon_import;
$this->row++;
$postmeta = array();
$term_data = array();
$coupon = array();
$post_id = ( ! empty( $item['id'] ) ) ? absint( $item['id'] ) : 0;
$post_id = ( ! empty( $item['post_id'] ) ) ? absint( $item['post_id'] ) : $post_id;
$product['post_id'] = $post_id;
// Get post fields.
foreach ( $this->post_defaults as $column => $default ) {
if ( isset( $item[ $column ] ) ) {
$product[ $column ] = $item[ $column ];
}
}
// Get custom fields.
foreach ( $this->postmeta_defaults as $column => $default ) {
if ( isset( $item[ $column ] ) ) {
$postmeta[ $column ] = (string) $item[ $column ];
} elseif ( isset( $item[ '_' . $column ] ) ) {
$postmeta[ $column ] = (string) $item[ '_' . $column ];
}
}
// Get term fields.
foreach ( $this->term_defaults as $column => $default ) {
if ( isset( $item[ $column ] ) ) {
$term_data[ $column ] = $item[ $column ];
}
}
// Merge post meta with defaults.
$coupon = wp_parse_args( $product, $this->post_defaults );
$postmeta = wp_parse_args( $postmeta, $this->postmeta_defaults );
$term_data = wp_parse_args( $term_data, $this->term_defaults );
$all_discount_types = wc_get_coupon_types();
if ( ! empty( $postmeta['discount_type'] ) ) {
$discount_type = __( $postmeta['discount_type'], 'woocommerce' ); // phpcs:ignore
} else {
if ( $this->is_wc_gte_30() ) {
$discount_type = __( 'Percentage discount', 'woocommerce' );
} else {
$discount_type = __( 'Cart % Discount', 'woocommerce' );
}
}
$discount_type = ( ! empty( $discount_type ) && in_array( $discount_type, $all_discount_types, true ) ) ? $discount_type : $postmeta['discount_type'];
// discount types.
if ( ! empty( $discount_type ) ) {
if ( in_array( $discount_type, $all_discount_types, true ) ) {
$postmeta['discount_type'] = array_search( $discount_type, $all_discount_types, true );
}
if ( empty( $postmeta['discount_type'] ) ) {
$postmeta['discount_type'] = 'percent';
}
}
// product_ids.
if ( isset( $postmeta['product_ids'] ) && ! is_array( $postmeta['product_ids'] ) ) {
$ids = array_filter( array_map( 'trim', explode( '|', $postmeta['product_ids'] ) ) );
$ids = implode( ',', $ids );
$postmeta['product_ids'] = $ids;
}
// exclude_product_ids.
if ( isset( $postmeta['exclude_product_ids'] ) && ! is_array( $postmeta['exclude_product_ids'] ) ) {
$ids = array_filter( array_map( 'trim', explode( '|', $postmeta['exclude_product_ids'] ) ) );
$ids = implode( ',', $ids );
$postmeta['exclude_product_ids'] = $ids;
}
// product_categories.
if ( isset( $postmeta['product_categories'] ) && ! is_array( $postmeta['product_categories'] ) ) {
$ids = array_filter( array_map( 'trim', explode( '|', $postmeta['product_categories'] ) ) );
$postmeta['product_categories'] = $ids;
}
// exclude_product_categories.
if ( isset( $postmeta['exclude_product_categories'] ) && ! is_array( $postmeta['exclude_product_categories'] ) ) {
$ids = array_filter( array_map( 'trim', explode( '|', $postmeta['exclude_product_categories'] ) ) );
$postmeta['exclude_product_categories'] = $ids;
}
// customer_email.
if ( isset( $postmeta['customer_email'] ) && ! is_array( $postmeta['customer_email'] ) ) {
$email_ids = array_filter( array_map( 'trim', explode( ',', $postmeta['customer_email'] ) ) );
$postmeta['customer_email'] = $email_ids;
}
// expiry date.
if ( isset( $postmeta['expiry_date'] ) ) {
$timestamp_expiry_date = '';
if ( ! empty( $postmeta['expiry_date'] ) ) {
$timestamp_expiry_date = ( function_exists( 'wc_string_to_timestamp' ) ) ? wc_string_to_timestamp( $postmeta['expiry_date'] ) : 0;
}
if ( ! empty( $postmeta['expiry_date'] ) && empty( $timestamp_expiry_date ) ) {
/* translators: 1. Coupon code 2. Expiry date */
$this->log( 'error', sprintf( __( 'Incorrect format for expiry date of coupon "%1$s". Entered date is %2$s. Expected date format: YYYY-MM-DD', 'woocommerce-smart-coupons' ), $coupon['post_title'], $postmeta['expiry_date'] ) );
}
$postmeta['expiry_date'] = ( ! empty( $timestamp_expiry_date ) ) ? gmdate( 'Y-m-d', $timestamp_expiry_date ) : '';
}
// usage count.
if ( isset( $postmeta['usage_count'] ) ) {
$postmeta['usage_count'] = ( ! empty( $postmeta['usage_count'] ) ) ? $postmeta['usage_count'] : 0;
}
// used_by.
if ( isset( $postmeta['_used_by'] ) ) {
$postmeta['_used_by'] = ( ! empty( $postmeta['_used_by'] ) ) ? $postmeta['_used_by'] : '';
}
// Put set core product postmeta into product array.
foreach ( $postmeta as $key => $value ) {
$coupon['postmeta'][] = array(
'key' => esc_attr( $key ),
'value' => $value,
);
}
// term data.
foreach ( $term_data as $key => $value ) {
$coupon['term_data'][] = array(
'key' => esc_attr( $key ),
'value' => $value,
);
}
unset( $item, $postmeta );
return $coupon;
}
}
}

View File

@@ -0,0 +1,862 @@
<?php
/**
* Processing of smart coupons refund
*
* @author StoreApps
* @since 5.2.0
* @version 2.1.0
*
* @package woocommerce-smart-coupons/includes/
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
if ( ! class_exists( 'WC_SC_Coupon_Refund_Process' ) ) {
/**
* Class for handling processes of smart coupons refund
*/
class WC_SC_Coupon_Refund_Process {
/**
* Variable to hold instance of WC_SC_Coupon_Refund_Process
*
* @var $instance
*/
private static $instance = null;
/**
* Constructor
*/
private function __construct() {
add_action( 'woocommerce_admin_order_items_after_shipping', array( $this, 'render_used_store_credits_details' ) );
add_action( 'woocommerce_admin_order_items_after_refunds', array( $this, 'render_refunded_store_credits_details' ) );
add_filter( 'woocommerce_order_fully_refunded_status', array( $this, 'update_fully_refunded_status' ), 10, 3 );
add_action( 'wp_ajax_wc_sc_refund_store_credit', array( $this, 'wc_sc_refund_store_credit' ) );
add_action( 'wp_ajax_wc_sc_revoke_refunded_store_credit', array( $this, 'wc_sc_revoke_refunded_store_credit' ) );
}
/**
* Get single instance of WC_SC_Coupon_Process
*
* @return WC_SC_Coupon_Process Singleton object of WC_SC_Coupon_Process
*/
public static function get_instance() {
// Check if instance is already exists.
if ( is_null( self::$instance ) ) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Handle call to functions which is not available in this class
*
* @param string $function_name The function name.
* @param array $arguments Array of arguments passed while calling $function_name.
* @return result of function call
*/
public function __call( $function_name, $arguments = array() ) {
global $woocommerce_smart_coupon;
if ( ! is_callable( array( $woocommerce_smart_coupon, $function_name ) ) ) {
return;
}
if ( ! empty( $arguments ) ) {
return call_user_func_array( array( $woocommerce_smart_coupon, $function_name ), $arguments );
} else {
return call_user_func( array( $woocommerce_smart_coupon, $function_name ) );
}
}
/**
* Render smart coupon UI in order page
*
* @param int $order_id order id.
* @return void html of smart coupon UI
*/
public function render_used_store_credits_details( $order_id = 0 ) {
?>
<style type="text/css">
#order_sc_store_credit_line_items tbody tr:first-child td {
border-top: 8px solid #f8f8f8;
}
</style>
<script type="text/javascript">
jQuery(function ($) {
/**
* Auto fill and remove - refund store credit coupon amount
*/
$(document).on('change', '.sc_auto_fill_refund', function () {
if ($(this).prop("checked") === true) {
$('.sc_store_credit').each(function (index, element) {
let used_sc = $(element).data("order_used_sc");
let order_used_sc_tax = $(element).data("order_used_sc_tax");
$(this).find('.refund_used_sc').val(used_sc);
$(this).find('.refund_used_sc_tax').val(order_used_sc_tax);
});
} else {
$('.refund_used_sc').val(0);
$('.refund_used_sc_tax').val(0);
}
});
/**
* Process store credit refund
*/
$('#woocommerce-order-items').on('click', 'button.do-api-refund, button.do-manual-refund', function () {
var sc_line_item = {};
$('.sc_store_credit').each(function (index, element) {
let sc_line_item_data = {};
let used_coupon_id = $(this).data("used_sc_id");
let order_id = $(this).data("used_sc_order_id");
let sc_refund_amount = $(this).find('.refund_used_sc').val();
let sc_refund_tax_amount = $(this).find('.refund_used_sc_tax').val();
let order_used_total_amount = $(this).find('.order_used_total_sc_amount').val();
let order_sc_item_id = $(this).find('.order_sc_item_id').val();
if (order_sc_item_id !== 0 || order_sc_item_id !== null || order_sc_item_id !== undefined) {
sc_line_item_data['coupon_id'] = used_coupon_id;
sc_line_item_data['refund_amount'] = sc_refund_amount;
sc_line_item_data['order_used_total_amount'] = order_used_total_amount;
sc_line_item_data['order_sc_item_id'] = order_sc_item_id;
sc_line_item_data['sc_order_id'] = order_id;
sc_line_item_data['order_sc_refund_tax_amount'] = sc_refund_tax_amount;
sc_line_item [order_sc_item_id] = sc_line_item_data;
}
});
let sc_refund_nonce = $('#sc_refund_nonce').val();
var data = {
action: 'wc_sc_refund_store_credit',
line_items: JSON.stringify(sc_line_item, null, ''),
security: sc_refund_nonce
};
$.ajax({
url: '<?php echo esc_url( admin_url( 'admin-ajax.php' ) ); ?>',
type: 'POST',
data: data
});
});
});
</script>
<tbody id="order_sc_store_credit_line_items">
<?php
$order = ( function_exists( 'wc_get_order' ) && ! empty( $order_id ) ) ? wc_get_order( $order_id ) : null;
$order_total = $this->is_callable( $order, 'get_total' ) ? $order->get_total() : 0;
$order_items = ( is_object( $order ) && $this->is_callable( $order, 'get_items' ) ) ? $order->get_items( 'coupon' ) : array();
$tax_data = ( function_exists( 'wc_tax_enabled' ) && wc_tax_enabled() && is_object( $order ) && $this->is_callable( $order, 'get_taxes' ) ) ? $order->get_taxes() : array();
if ( ! class_exists( 'WC_SC_Order_Fields' ) ) {
include_once 'class-wc-sc-order-fields.php';
}
$sc_order_fields = WC_SC_Order_Fields::get_instance();
$total_credit_used_in_order = $sc_order_fields->get_total_credit_used_in_order( $order );
$i = 1;
if ( ! empty( $order_items ) ) {
$is_apply_before_tax = get_option( 'woocommerce_smart_coupon_apply_before_tax', 'no' );
$is_readonly = ( 'yes' !== $is_apply_before_tax ) ? true : false;
$allow_custom_refund_amount = apply_filters(
'wc_sc_allow_custom_refund_amount',
( false === $is_readonly ),
array(
'source' => $this,
'order_obj' => $order,
)
);
$is_old_sc_order = $this->is_old_sc_order( $order_id );
$item_titles = array_map(
function( $item ) {
return ( $this->is_callable( $item, 'get_name' ) ) ? $item->get_name() : '';
},
$order_items
);
$posts = $this->get_post_by_title( $item_titles, OBJECT, 'shop_coupon' );
foreach ( $order_items as $item_id => $item ) {
$order_discount_amount = $sc_refunded_discount = $sc_refunded_discount_tax = $order_discount_tax_amount = 0; // phpcs:ignore
$coupon_code = ( $this->is_callable( $item, 'get_name' ) ) ? $item->get_name() : '';
$sanitized_coupon_code = sanitize_title( $coupon_code ); // The generated string will be checked in an array key to locate post object.
$coupon_post_obj = ( ! empty( $posts[ $sanitized_coupon_code ] ) ) ? $posts[ $sanitized_coupon_code ] : null;
$coupon_id = isset( $coupon_post_obj->ID ) ? $coupon_post_obj->ID : '';
$coupon_title = isset( $coupon_post_obj->post_title ) ? $coupon_post_obj->post_title : '';
$coupon = new WC_Coupon( $coupon_id );
if ( is_a( $coupon, 'WC_Coupon' ) ) {
if ( $coupon->is_type( 'smart_coupon' ) ) {
if ( 1 === $i ) {
?>
<tr class="sc_store_credit_head refund">
<td class="">
<div class="refund" style="display: none; padding-top: 15px;">
<svg xmlns="http://www.w3.org/2000/svg" style="color: #b5b5b5;" fill="none" viewBox="0 0 40 40" stroke="currentColor" class="w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1" d="M15 5v2m0 4v2m0 4v2M5 5a2 2 0 00-2 2v3a2 2 0 110 4v3a2 2 0 002 2h14a2 2 0 002-2v-3a2 2 0 110-4V7a2 2 0 00-2-2H5z">
</path>
</svg>
</div>
</td>
<td>
<div class="refund" style="display: none; padding-top: 15px;">
<div class="view"><?php echo esc_html__( 'Refund to Store Credit', 'woocommerce-smart-coupons' ); ?></div>
</div>
</td>
<td>
</td>
<td>
<input type="hidden" name="sc_refund_nonce" id="sc_refund_nonce" value="<?php echo esc_attr( wp_create_nonce( 'sc_refund_nonce' ) ); ?>">
</td>
<td colspan="2">
<?php if ( true === $is_old_sc_order || 'yes' === $is_apply_before_tax ) { ?>
<div class="refund" style="display: none; padding-top: 15px;">
<input type="checkbox" name="sc_auto_fill_refund" class="sc_auto_fill_refund" id="sc_auto_fill_refund">
<label for="sc_auto_fill_refund"><?php echo esc_html_e( 'Auto-fill refund amount', 'woocommerce-smart-coupons' ); ?></label>
</div>
<?php } ?>
</td>
<?php
if ( ! empty( $tax_data ) ) {
$tax_data_count = count( $tax_data );
?>
<td colspan="<?php echo esc_attr( ( $tax_data_count ) ); ?>">
</td>
<?php
}
?>
</tr>
<?php
}
$i++;
if ( is_callable( array( $this, 'get_order_item_meta' ) ) ) {
$order_discount_amount = (float) $this->get_order_item_meta( $item_id, 'discount_amount', true );
$sc_refunded_discount = (float) $this->get_order_item_meta( $item_id, 'sc_refunded_discount', true );
$sc_refunded_discount_tax = (float) $this->get_order_item_meta( $item_id, 'sc_refunded_discount_tax', true );
$order_discount_tax_amount = (float) $this->get_order_item_meta( $item_id, 'discount_amount_tax', true );
}
?>
<tr class="sc_store_credit" data-order_used_sc="<?php echo esc_attr( $order_discount_amount ); ?>"
data-used_sc_id="<?php echo esc_attr( $coupon_id ); ?>"
data-used_sc_order_id="<?php echo esc_attr( $order_id ); ?>"
data-order_used_sc_tax="<?php echo esc_attr( $order_discount_tax_amount ); ?>">
<td class="thumb">
<div>
<svg xmlns="http://www.w3.org/2000/svg" style="color: #b5b5b5;" fill="none" viewBox="0 0 40 40" stroke="currentColor" class="w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1" d="M15 5v2m0 4v2m0 4v2M5 5a2 2 0 00-2 2v3a2 2 0 110 4v3a2 2 0 002 2h14a2 2 0 002-2v-3a2 2 0 110-4V7a2 2 0 00-2-2H5z">
</path>
</svg>
</div>
</td>
<td class="name">
<div class="view">
<?php
echo esc_html( $coupon_title ); // phpcs:ignore
?>
</div>
</td>
<td class="item_cost" width="1%">&nbsp;</td>
<td class="quantity" width="1%">&nbsp;</td>
<td class="line_cost" width="1%">
<div class="view">
<?php
echo wp_kses_post(
wc_price( $order_discount_amount )
);
if ( ! empty( $sc_refunded_discount ) ) {
?>
<small class="refunded"><?php echo wp_kses_post( wc_price( $sc_refunded_discount ) ); ?></small>
<?php
}
$max_refund_limit = $order_discount_amount - $sc_refunded_discount;
?>
</div>
<div class="edit" style="display: none;">
<input type="text" name="sc_store_credit_cost[<?php echo esc_attr( $coupon_id ); ?>]" placeholder="0" value="<?php echo esc_attr( $order_discount_amount ); ?>" class="sc_line_total wc_input_price">
</div>
<?php if ( $sc_refunded_discount < $order_discount_amount ) { ?>
<div class="refund" style="display: none;">
<input type="text" name="refund_sc_store_credit_line_total[<?php echo esc_attr( $coupon_id ); ?>]" placeholder="0" class="refund_used_sc wc_input_price" max="<?php echo esc_attr( $max_refund_limit ); ?>" <?php ( ( false === $allow_custom_refund_amount ) ? esc_attr_e( 'readonly' ) : '' ); ?>>
<input type="hidden" name="order_used_total_sc_amount[<?php echo esc_attr( $coupon_id ); ?>]" value="<?php echo esc_attr( $order_discount_amount ); ?>" class="order_used_total_sc_amount">
<input type="hidden" name="order_used_item_id[<?php echo esc_attr( $item_id ); ?>]" value="<?php echo esc_attr( $item_id ); ?>" class="order_sc_item_id">
</div>
<?php } ?>
</td>
<?php
if ( ! empty( $tax_data ) ) {
$tax_data_count = count( $tax_data );
?>
<td class="line_tax" width="1%">
<div class="view">
<?php
echo wp_kses_post(
wc_price( $order_discount_tax_amount )
);
if ( ! empty( $sc_refunded_discount_tax ) ) {
?>
<small class="refunded"><?php echo wp_kses_post( wc_price( $sc_refunded_discount_tax ) ); ?></small>
<?php
}
$max_refund_tax_limit = $order_discount_tax_amount - $sc_refunded_discount_tax;
?>
</div>
<div class="edit" style="display: none;">
<input type="text" name="sc_store_credit_cost[<?php echo esc_attr( $coupon_id ); ?>]" placeholder="0" value="<?php echo esc_attr( $order_discount_tax_amount ); ?>" class="sc_line_total wc_input_price">
</div>
<?php if ( $sc_refunded_discount_tax < $order_discount_tax_amount ) { ?>
<div class="refund" style="display: none;">
<input type="text" name="refund_sc_store_credit_line_total[<?php echo esc_attr( $coupon_id ); ?>]" placeholder="0" class="refund_used_sc_tax wc_input_price" max="<?php echo esc_attr( $max_refund_tax_limit ); ?>" <?php ( ( false === $allow_custom_refund_amount ) ? esc_attr_e( 'readonly' ) : '' ); ?>>
</div>
<?php } ?>
</td>
<?php if ( $tax_data_count > 1 ) { ?>
<td colspan="<?php echo esc_attr( ( $tax_data_count - 1 ) ); ?>"></td>
<?php } ?>
<?php } ?>
<td class="wc-order-edit-line-item">
</td>
</tr>
<?php
}
}
}
}
?>
</tbody>
<?php if ( ! $this->is_old_sc_order( $order_id ) && $total_credit_used_in_order > 0 ) { ?>
<script type="text/javascript">
jQuery(function(){
function wc_sc_reload_refund_amount() {
var order_total = '<?php echo esc_html( $order_total ); ?>';
var refund_amount = jQuery('#refund_amount').val();
var wc_sc_refund_line_total = 0;
var wc_sc_refund_line_tax = 0;
jQuery('.line_cost .refund .refund_line_total').each(function() {
wc_sc_refund_line_unit_total = accounting.unformat( jQuery(this).val(), woocommerce_admin.mon_decimal_point );
wc_sc_refund_line_total += wc_sc_refund_line_unit_total;
});
jQuery('.line_tax .refund .refund_line_tax').each(function() {
wc_sc_refund_line_unit_tax = accounting.unformat( jQuery(this).val(), woocommerce_admin.mon_decimal_point );
wc_sc_refund_line_tax += wc_sc_refund_line_unit_tax;
});
jQuery('.refund_used_sc').each(function(){
var max_limit = accounting.unformat( jQuery(this).attr('max') );
wc_sc_refund_line_total = accounting.unformat( wc_sc_refund_line_total, woocommerce_admin.mon_decimal_point );
var new_refund_amount = Math.min( wc_sc_refund_line_total, max_limit );
jQuery(this).val(
accounting.formatNumber(
new_refund_amount,
woocommerce_admin_meta_boxes.currency_format_num_decimals,
'',
woocommerce_admin.mon_decimal_point
)
);
wc_sc_refund_line_total -= new_refund_amount;
});
jQuery('.refund_used_sc_tax').each(function(){
var max_limit = accounting.unformat( jQuery(this).attr('max') );
wc_sc_refund_line_tax = accounting.unformat( wc_sc_refund_line_tax, woocommerce_admin.mon_decimal_point );
var new_refund_amount = Math.min( wc_sc_refund_line_tax, max_limit );
jQuery(this).val(
accounting.formatNumber(
new_refund_amount,
woocommerce_admin_meta_boxes.currency_format_num_decimals,
'',
woocommerce_admin.mon_decimal_point
)
);
wc_sc_refund_line_tax -= new_refund_amount;
});
refund_amount = wc_sc_refund_line_total + wc_sc_refund_line_tax;
if ( refund_amount > 0 ) {
refund_amount = Math.min(refund_amount, order_total);
}
jQuery('button.do-api-refund, button.do-manual-refund').find('.wc-order-refund-amount .amount' ).text( accounting.formatMoney( refund_amount, {
symbol: woocommerce_admin_meta_boxes.currency_format_symbol,
decimal: woocommerce_admin_meta_boxes.currency_format_decimal_sep,
thousand: woocommerce_admin_meta_boxes.currency_format_thousand_sep,
precision: woocommerce_admin_meta_boxes.currency_format_num_decimals,
format: woocommerce_admin_meta_boxes.currency_format
} ) );
if ( refund_amount <= 0 ) {
jQuery('#refund_amount').val('');
jQuery('button.do-api-refund').hide();
} else {
jQuery('#refund_amount').val(
accounting.formatNumber(
refund_amount,
woocommerce_admin_meta_boxes.currency_format_num_decimals,
'',
woocommerce_admin.mon_decimal_point
)
);
jQuery('button.do-api-refund').show();
}
}
jQuery(document).on('change', '#refund_amount', function(){
wc_sc_reload_refund_amount();
});
});
</script>
<?php } ?>
<?php
}
/**
* Refund store credit coupon
*
* @return void add refund store credit to the coupon
*/
public function wc_sc_refund_store_credit() {
global $woocommerce_smart_coupon;
$nonce_token = ! empty( $_POST['security'] ) ? $_POST['security'] : ''; // phpcs:ignore
if ( wp_verify_nonce( wp_unslash( $nonce_token ), 'sc_refund_nonce' ) ) {
$response = array();
$line_items = isset( $_POST['line_items'] ) ? json_decode( sanitize_text_field( wp_unslash( $_POST['line_items'] ) ), true ) : array();
if ( ! empty( $line_items ) ) {
$order = null;
foreach ( $line_items as $line_item ) {
$total_refunded = $total_refunded_tax = $order_discount_amount = $refunded_amount = $refunded_tax_amount = $order_discount_tax_amount = 0; // phpcs:ignore
$smart_coupon_id = ( ! empty( $line_item['coupon_id'] ) ) ? $line_item['coupon_id'] : 0;
$refund_amount = ( ! empty( $line_item['refund_amount'] ) ) ? wc_format_decimal( $line_item['refund_amount'] ) : 0;
$item_id = ( ! empty( $line_item['order_sc_item_id'] ) ) ? $line_item['order_sc_item_id'] : 0;
$order_sc_refund_tax_amount = ( ! empty( $line_item['order_sc_refund_tax_amount'] ) ) ? wc_format_decimal( $line_item['order_sc_refund_tax_amount'] ) : 0;
$order_id = ( ! empty( $line_item['sc_order_id'] ) ) ? $line_item['sc_order_id'] : 0;
$order = ( ! is_a( $order, 'WC_Order' ) ) ? wc_get_order( $order_id ) : $order;
if ( is_callable( array( $this, 'get_order_item_meta' ) ) ) {
$order_discount_amount = $this->get_order_item_meta( $item_id, 'discount_amount', true );
$refunded_amount = $this->get_order_item_meta( $item_id, 'sc_refunded_discount', true );
$refunded_tax_amount = $this->get_order_item_meta( $item_id, 'sc_refunded_discount_tax', true );
$order_discount_tax_amount = $this->get_order_item_meta( $item_id, 'discount_amount_tax', true );
}
if ( floatval( $order_discount_amount ) === floatval( $refunded_amount ) && floatval( $order_discount_tax_amount ) === floatval( $refunded_tax_amount ) ) {
continue;
}
if ( $refunded_amount ) {
$total_refunded = $refund_amount + $refunded_amount;
}
if ( $refunded_tax_amount ) {
$total_refunded_tax = $order_sc_refund_tax_amount + $refunded_tax_amount;
}
if ( $order_discount_amount >= $refund_amount && $order_discount_amount >= $total_refunded && $order_discount_tax_amount >= $order_sc_refund_tax_amount && $order_discount_tax_amount >= $total_refunded_tax && ! empty( $smart_coupon_id ) ) {
$coupon = new WC_Coupon( $smart_coupon_id );
if ( is_a( $coupon, 'WC_Coupon' ) ) {
if ( $this->is_wc_gte_30() ) {
$discount_type = ( is_callable( array( $coupon, 'get_discount_type' ) ) ) ? $coupon->get_discount_type() : '';
} else {
$discount_type = ( ! empty( $coupon->discount_type ) ) ? $coupon->discount_type : '';
}
$coupon_amount = $this->get_amount( $coupon, true, $order );
if ( 'smart_coupon' === $discount_type && is_numeric( $refund_amount ) && is_numeric( $order_sc_refund_tax_amount ) ) {
$amount = $coupon_amount + $refund_amount + $order_sc_refund_tax_amount;
$this->update_post_meta( $smart_coupon_id, 'coupon_amount', $amount, true, $order );
$user = ( function_exists( 'get_current_user_id' ) ) ? get_current_user_id() : 0;
$local_time = ( function_exists( 'current_datetime' ) ) ? current_datetime() : '';
$get_timestamp = ( is_object( $local_time ) && is_callable( array( $local_time, 'getTimestamp' ) ) ) ? $local_time->getTimestamp() : '';
$get_offset = ( is_object( $local_time ) && is_callable( array( $local_time, 'getOffset' ) ) ) ? $local_time->getOffset() : '';
$current_time_stamp = $get_timestamp + $get_offset;
if ( 0 < $total_refunded ) {
$refund_amount = $total_refunded;
}
if ( 0 < $total_refunded_tax ) {
$order_sc_refund_tax_amount = $total_refunded_tax;
}
if ( is_callable( array( $this, 'update_order_item_meta' ) ) ) {
$this->update_order_item_meta( $item_id, 'sc_refunded_discount_tax', $order_sc_refund_tax_amount );
$this->update_order_item_meta( $item_id, 'sc_refunded_discount', $refund_amount );
$this->update_order_item_meta( $item_id, 'sc_refunded_user_id', $user );
$this->update_order_item_meta( $item_id, 'sc_refunded_timestamp', $current_time_stamp );
$this->update_order_item_meta( $item_id, 'sc_refunded_coupon_id', $smart_coupon_id );
$message = __( 'Successfully updated store credit refund details.', 'woocommerce-smart-coupons' );
} else {
$message = __( 'Failed to update store credit refund details.', 'woocommerce-smart-coupons' );
$woocommerce_smart_coupon->log( 'notice', $message . ' ' . __FILE__ . ' ' . __LINE__ );
}
$response['message'] = $message;
}
}
}
}
}
wp_send_json_success( $response );
} else {
$response['message'] = __( 'Nonce verification failed for action "wc_sc_refund_store_credit".', 'woocommerce-smart-coupons' );
wp_send_json_error( $response );
}
}
/**
* Render refund store credit UI in order page
*
* @param int $order_id order id.
* @return void revoke refund html
*/
public function render_refunded_store_credits_details( $order_id ) {
global $store_credit_label;
?>
<style type="text/css">
#woocommerce-order-items .wc-order-edit-line-item-actions .delete_wc_sc_refund::before {
font-family: Dashicons;
font-weight: 400;
text-transform: none;
line-height: 1;
-webkit-font-smoothing: antialiased;
text-indent: 0px;
top: 0px;
left: 0px;
width: 100%;
height: 100%;
text-align: center;
content: "";
position: relative;
font-variant: normal;
margin: 0px;
color: #a00;
}
</style>
<script type="text/javascript">
jQuery(function ($) {
/**
* Process store credit revoke refund
*/
var wc_sc_meta_boxes_order_items = {
block: function () {
$('#woocommerce-order-items').block({
message: null,
overlayCSS: {
background: '#fff',
opacity: 0.6
}
});
},
};
$('#woocommerce-order-items').on('click', '.delete_wc_sc_refund', function () {
var confirm_message = '<?php esc_html_e( 'Are you sure you wish to delete this refund? This action cannot be undone.', 'woocommerce-smart-coupons' ); ?>';
if (confirm(confirm_message)) {
let sc_line_item_data = {};
let wc_sc_refunded_id = $(this).parents('.wc_sc_refunded').data("wc_sc_refunded_id");
let order_id = $(this).parents('.wc_sc_refunded').data("wc_sc_order_id");
let wc_sc_id = $(this).parents('.wc_sc_refunded').data("wc_sc_coupon_id");
let wc_sc_refunded_amount = $(this).parents('.wc_sc_refunded').data("wc_sc_refunded_amount");
let wc_sc_refunded_amount_tax = $(this).parents('.wc_sc_refunded').data("wc_sc_refunded_amount_tax");
if (wc_sc_refunded_id !== 0 || wc_sc_refunded_id !== null || wc_sc_refunded_id !== undefined) {
sc_line_item_data['coupon_id'] = wc_sc_id;
sc_line_item_data['refund_amount'] = wc_sc_refunded_amount;
sc_line_item_data['refund_amount_tax'] = wc_sc_refunded_amount_tax;
sc_line_item_data['wc_sc_refunded_id'] = wc_sc_refunded_id;
sc_line_item_data['wc_sc_order_id'] = order_id;
}
let sc_revoke_refund_nonce = $('#sc_revoke_refund_nonce').val();
var data = {
action: 'wc_sc_revoke_refunded_store_credit',
line_items: JSON.stringify(sc_line_item_data, null, ''),
security: sc_revoke_refund_nonce
};
wc_sc_meta_boxes_order_items.block();
$.ajax({
url: '<?php echo esc_url( admin_url( 'admin-ajax.php' ) ); ?>',
type: 'POST',
data: data,
success: function (response) {
if (true === response.success) {
// Redirect to same page for show the refunded status
window.location.reload();
} else {
console.log(response.data.error);
}
}
});
}
})
});
</script>
<?php
$order = wc_get_order( $order_id );
$order_items = ( is_object( $order ) && is_callable( array( $order, 'get_items' ) ) ) ? $order->get_items( 'coupon' ) : array();
$tax_data = ( function_exists( 'wc_tax_enabled' ) && wc_tax_enabled() && is_object( $order ) && is_callable( array( $order, 'get_taxes' ) ) ) ? $order->get_taxes() : array();
if ( ! empty( $order_items ) ) {
$item_titles = array_map(
function( $item ) {
return $item->get_name();
},
$order_items
);
$posts = $this->get_post_by_title( $item_titles, OBJECT, 'shop_coupon' );
foreach ( $order_items as $item_id => $item ) {
$sc_refunded_discount = $sc_refunded_discount_tax = $sc_refunded_user = $sc_refunded_timestamp = 0; // phpcs:ignore
$coupon_code = ( $this->is_callable( $item, 'get_name' ) ) ? $item->get_name() : '';
$sanitized_coupon_code = sanitize_title( $coupon_code ); // The generated string will be checked in an array key to locate post object.
$coupon_post_obj = ( ! empty( $posts[ $sanitized_coupon_code ] ) ) ? $posts[ $sanitized_coupon_code ] : null;
$coupon_id = isset( $coupon_post_obj->ID ) ? $coupon_post_obj->ID : '';
$coupon_title = isset( $coupon_post_obj->post_title ) ? $coupon_post_obj->post_title : '';
$coupon = new WC_Coupon( $coupon_id );
if ( is_callable( array( $this, 'get_order_item_meta' ) ) ) {
$sc_refunded_discount = (float) $this->get_order_item_meta( $item_id, 'sc_refunded_discount', true );
$sc_refunded_discount_tax = (float) $this->get_order_item_meta( $item_id, 'sc_refunded_discount_tax', true );
$sc_refunded_user = $this->get_order_item_meta( $item_id, 'sc_refunded_user_id', true );
$sc_refunded_timestamp = $this->get_order_item_meta( $item_id, 'sc_refunded_timestamp', true );
}
if ( empty( $sc_refunded_timestamp ) ) {
$sc_refunded_timestamp = time() + $this->wc_timezone_offset();
}
$sc_refunded_discount = empty( $sc_refunded_discount ) ? 0 : $sc_refunded_discount;
$sc_refunded_discount_tax = empty( $sc_refunded_discount_tax ) ? 0 : $sc_refunded_discount_tax;
if ( is_a( $coupon, 'WC_Coupon' ) ) {
if ( $coupon->is_type( 'smart_coupon' ) && ( ! empty( $sc_refunded_discount ) || ! empty( $sc_refunded_discount_tax ) ) ) {
$who_refunded = new WP_User( $sc_refunded_user );
$refunder_id = isset( $who_refunded->ID ) ? $who_refunded->ID : 0;
$refunder_name = isset( $who_refunded->display_name ) ? $who_refunded->display_name : '';
?>
<input type="hidden" name="sc_revoke_refund_nonce" id="sc_revoke_refund_nonce" value="<?php echo esc_attr( wp_create_nonce( 'sc_revoke_refund_nonce' ) ); ?>">
<tr class="refund wc_sc_refunded" data-wc_sc_refunded_id="<?php echo esc_attr( $item_id ); ?>"
data-wc_sc_coupon_id="<?php echo esc_attr( $coupon_id ); ?>"
data-wc_sc_order_id="<?php echo esc_attr( $order_id ); ?>"
data-wc_sc_refunded_amount_tax="<?php echo esc_attr( $sc_refunded_discount_tax ); ?>"
data-wc_sc_refunded_amount="<?php echo esc_attr( $sc_refunded_discount ); ?>">
<td class="thumb">
<div></div>
</td>
<td class="name">
<?php
if ( $who_refunded->exists() ) {
printf(
/* translators: 1: refund id 2: refund date 3: username */
esc_html__( 'Refund %1$s - %2$s by %3$s', 'woocommerce-smart-coupons' ),
sprintf( '%s - %s', ( ! empty( $store_credit_label['singular'] ) ? esc_html( $store_credit_label['singular'] ) : esc_html__( 'Store Credit', 'woocommerce-smart-coupons' ) ), esc_html( $coupon_title ) ),
esc_html( $this->format_date( $sc_refunded_timestamp ) ),
sprintf(
'<abbr class="refund_by" title="%1$s">%2$s</abbr>',
/* translators: 1: ID who refunded */
sprintf( esc_attr__( 'ID: %d', 'woocommerce-smart-coupons' ), absint( $refunder_id ) ),
esc_html( $refunder_name )
)
);
} else {
printf(
/* translators: 1: refund id 2: refund date */
esc_html__( 'Refund %1$s - %2$s', 'woocommerce-smart-coupons' ),
sprintf( '%s - %s', ( ! empty( $store_credit_label['singular'] ) ? esc_html( $store_credit_label['singular'] ) : esc_html__( 'Store Credit', 'woocommerce-smart-coupons' ) ), esc_html( $coupon_title ) ),
esc_html( $this->format_date( $sc_refunded_timestamp ) )
);
}
?>
</td>
<td class="item_cost" width="1%">&nbsp;</td>
<td class="quantity" width="1%">&nbsp;</td>
<td class="line_cost" width="1%">
<div class="view">
<?php
$total = $sc_refunded_discount + $sc_refunded_discount_tax;
echo wp_kses_post(
wc_price( '-' . $total )
);
?>
</div>
</td>
<?php if ( ! empty( $tax_data ) ) : ?>
<td class="line_tax" width="1%"></td>
<?php
$tax_data_count = count( $tax_data );
if ( $tax_data_count > 1 ) {
?>
<td colspan="<?php echo esc_attr( $tax_data_count - 1 ); ?>"></td>
<?php } ?>
<?php endif; ?>
<td class="wc-order-edit-line-item">
<div class="wc-order-edit-line-item-actions">
<a class="delete_wc_sc_refund" href="#" style=""></a>
</div>
</td>
</tr>
<?php
}
}
}
}
}
/**
* Formatting the date
*
* @param timestamp $date timestamp for date.
* @param string $format date format.
* @return string date format string
*/
protected function format_date( $date, $format = '' ) {
if ( ! is_int( $date ) ) {
$date = intval( $date );
}
if ( empty( $format ) ) {
$format = get_option( 'date_format', 'F j, Y' ) . ' ' . get_option( 'time_format', 'g:i a' );
}
return gmdate( $format, $date );
}
/**
* Revoke refund store credit
*
* @return void remove refund store credit amount to the coupon
*/
public function wc_sc_revoke_refunded_store_credit() {
$nonce_token = ! empty( $_POST['security'] ) ? $_POST['security'] : ''; // phpcs:ignore
if ( wp_verify_nonce( wp_unslash( $nonce_token ), 'sc_revoke_refund_nonce' ) ) {
$line_item = ! empty( $_POST['line_items'] ) ? json_decode( sanitize_text_field( wp_unslash( $_POST['line_items'] ) ), true ) : array();
if ( ! empty( $line_item ) ) {
$smart_coupon_id = ! empty( $line_item['coupon_id'] ) ? $line_item['coupon_id'] : 0;
$refunded_amount = ! empty( $line_item['refund_amount'] ) ? $line_item['refund_amount'] : 0;
$refunded_amount_tax = ! empty( $line_item['refund_amount_tax'] ) ? $line_item['refund_amount_tax'] : 0;
$wc_sc_refunded_id = ! empty( $line_item['wc_sc_refunded_id'] ) ? $line_item['wc_sc_refunded_id'] : 0;
$order_id = ! empty( $line_item['wc_sc_order_id'] ) ? $line_item['wc_sc_order_id'] : 0;
$order = ( ! is_a( $order, 'WC_Order' ) ) ? wc_get_order( $order_id ) : $order;
$order_discount_amount = $this->get_order_item_meta( $wc_sc_refunded_id, 'discount_amount', true );
$order_discount_tax_amount = $this->get_order_item_meta( $wc_sc_refunded_id, 'discount_amount_tax', true );
if ( $order_discount_amount >= $refunded_amount && $order_discount_tax_amount >= $refunded_amount_tax && ! empty( $smart_coupon_id ) ) {
$coupon = new WC_Coupon( $smart_coupon_id );
if ( is_a( $coupon, 'WC_Coupon' ) ) {
if ( $this->is_wc_gte_30() ) {
$discount_type = ( is_callable( array( $coupon, 'get_discount_type' ) ) ) ? $coupon->get_discount_type() : '';
} else {
$discount_type = ( ! empty( $coupon->discount_type ) ) ? $coupon->discount_type : '';
}
$coupon_amount = $this->get_amount( $coupon, true, $order );
if ( 'smart_coupon' === $discount_type ) {
if ( ! is_numeric( $refunded_amount ) ) {
$refunded_amount = 0;
}
if ( ! is_numeric( $refunded_amount_tax ) ) {
$refunded_amount_tax = 0;
}
$refund_amount = $refunded_amount + $refunded_amount_tax;
$amount = $coupon_amount - $refund_amount;
$this->update_post_meta( $smart_coupon_id, 'coupon_amount', $amount, true, $order );
$user = ( function_exists( 'get_current_user_id' ) ) ? get_current_user_id() : 0;
$local_time = ( function_exists( 'current_datetime' ) ) ? current_datetime() : '';
$get_timestamp = ( is_object( $local_time ) && is_callable( array( $local_time, 'getTimestamp' ) ) ) ? $local_time->getTimestamp() : 0;
$get_offset = ( is_object( $local_time ) && is_callable( array( $local_time, 'getOffset' ) ) ) ? $local_time->getOffset() : 0;
$current_time_stamp = $get_timestamp + $get_offset;
if ( is_callable( array( $this, 'update_order_item_meta' ) ) ) {
$this->update_order_item_meta( $wc_sc_refunded_id, 'sc_revoke_refunded_discount', $refunded_amount, true );
$this->update_order_item_meta( $wc_sc_refunded_id, 'sc_revoke_refunded_discount_tax', $refunded_amount_tax, true );
$this->update_order_item_meta( $wc_sc_refunded_id, 'sc_revoke_refunded_user_id', $user );
$this->update_order_item_meta( $wc_sc_refunded_id, 'sc_revoke_refunded_timestamp', $current_time_stamp );
$this->update_order_item_meta( $wc_sc_refunded_id, 'sc_revoke_refunded_coupon_id', $smart_coupon_id );
$this->update_order_item_meta( $wc_sc_refunded_id, 'sc_refunded_discount', 0 );
$this->update_order_item_meta( $wc_sc_refunded_id, 'sc_refunded_discount_tax', 0 );
}
}
}
}
}
wp_send_json_success();
} else {
wp_send_json_error();
}
}
/**
* Change order status when fully refunded.
*
* @param string $status order status.
* @param number $order_id order id.
* @param number $refund_id refund id.
* @return false|mixed order status
*/
public function update_fully_refunded_status( $status, $order_id, $refund_id ) {
$order = wc_get_order( $order_id );
$order_items = ( is_object( $order ) && is_callable( array( $order, 'get_items' ) ) ) ? $order->get_items( 'coupon' ) : array();
if ( ! empty( $order_items ) ) {
$item_titles = array_map(
function( $item ) {
return $item->get_name();
},
$order_items
);
$posts = $this->get_post_by_title( $item_titles, OBJECT, 'shop_coupon' );
foreach ( $order_items as $item_id => $item ) {
$sc_refunded_discount = $sc_refunded_discount_tax = $order_discount_amount = $order_discount_tax_amount = 0; // phpcs:ignore
$coupon_code = ( $this->is_callable( $item, 'get_name' ) ) ? $item->get_name() : '';
$sanitized_coupon_code = sanitize_title( $coupon_code ); // The generated string will be checked in an array key to locate post object.
$coupon_post_obj = ( ! empty( $posts[ $sanitized_coupon_code ] ) ) ? $posts[ $sanitized_coupon_code ] : null;
$coupon_id = isset( $coupon_post_obj->ID ) ? $coupon_post_obj->ID : '';
$coupon = new WC_Coupon( $coupon_id );
if ( is_callable( array( $this, 'get_order_item_meta' ) ) ) {
$sc_refunded_discount = $this->get_order_item_meta( $item_id, 'sc_refunded_discount', true );
$sc_refunded_discount_tax = $this->get_order_item_meta( $item_id, 'sc_refunded_discount_tax', true );
$order_discount_amount = $this->get_order_item_meta( $item_id, 'discount_amount', true );
$order_discount_tax_amount = $this->get_order_item_meta( $item_id, 'discount_amount_tax', true );
}
if ( is_a( $coupon, 'WC_Coupon' ) ) {
if ( $coupon->is_type( 'smart_coupon' ) ) {
$get_order_total = ( is_object( $order ) && is_callable( array( $order, 'get_total' ) ) ) ? $order->get_total() : 0;
$refunded_order_total = ( is_object( $order ) && is_callable( array( $order, 'get_total_refunded' ) ) ) ? $order->get_total_refunded() : 0;
if ( empty( $get_order_total ) && empty( $sc_refunded_discount_tax ) && empty( $order_discount_tax_amount ) && $order_discount_amount !== $sc_refunded_discount ) {
return false;
} elseif ( empty( $get_order_total ) && ! empty( $sc_refunded_discount_tax ) && ! empty( $order_discount_tax_amount ) && $order_discount_amount !== $sc_refunded_discount && $sc_refunded_discount_tax !== $order_discount_tax_amount ) {
return false;
} elseif ( ! empty( $get_order_total ) && empty( $sc_refunded_discount_tax ) && empty( $order_discount_tax_amount ) && $get_order_total === $refunded_order_total && $order_discount_amount !== $sc_refunded_discount ) {
return false;
} elseif ( ! empty( $get_order_total ) && ! empty( $sc_refunded_discount_tax ) && ! empty( $order_discount_tax_amount ) && $get_order_total === $refunded_order_total && $order_discount_amount !== $sc_refunded_discount && $sc_refunded_discount_tax !== $order_discount_tax_amount ) {
return false;
} elseif ( empty( $sc_refunded_discount_tax ) && ! empty( $order_discount_tax_amount ) ) {
return false;
}
}
}
}
}
return $status;
}
}
}
WC_SC_Coupon_Refund_Process::get_instance();

View File

@@ -0,0 +1,573 @@
<?php
/**
* Class to handle feature Coupons By Excluded Email
*
* @author StoreApps
* @category Admin
* @package wocommerce-smart-coupons/includes
* @since 6.7.0
* @version 1.6.1
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
if ( ! class_exists( 'WC_SC_Coupons_By_Excluded_Email' ) ) {
/**
* Class WC_SC_Coupons_By_Excluded_Email
*/
class WC_SC_Coupons_By_Excluded_Email {
/**
* Variable to hold instance of this class
*
* @var $instance
*/
private static $instance = null;
/**
* Constructor
*/
private function __construct() {
add_action( 'wc_sc_start_coupon_options_email_restriction', array( $this, 'usage_restriction' ) );
add_action( 'woocommerce_coupon_options_save', array( $this, 'process_meta' ), 10, 2 );
add_filter( 'woocommerce_coupon_is_valid', array( $this, 'validate' ), 11, 3 );
add_action( 'woocommerce_after_checkout_validation', array( $this, 'check_customer_coupons' ), 99, 2 );
add_filter( 'wc_smart_coupons_export_headers', array( $this, 'export_headers' ) );
add_filter( 'smart_coupons_parser_postmeta_defaults', array( $this, 'postmeta_defaults' ) );
add_filter( 'is_protected_meta', array( $this, 'make_action_meta_protected' ), 10, 3 );
add_filter( 'sc_generate_coupon_meta', array( $this, 'generate_coupon_meta' ), 10, 2 );
add_filter( 'wc_sc_process_coupon_meta_value_for_import', array( $this, 'import_coupon_meta' ), 10, 2 );
add_action( 'wc_sc_new_coupon_generated', array( $this, 'copy_coupon_meta' ) );
add_filter( 'manage_shop_coupon_posts_columns', array( $this, 'define_columns' ), 12 );
add_action( 'manage_shop_coupon_posts_custom_column', array( $this, 'render_columns' ), 10, 2 );
}
/**
* Get single instance of this class
*
* @return this class Singleton object of this class
*/
public static function get_instance() {
// Check if instance is already exists.
if ( is_null( self::$instance ) ) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Handle call to functions which is not available in this class
*
* @param string $function_name The function name.
* @param array $arguments Array of arguments passed while calling $function_name.
* @return result of function call
*/
public function __call( $function_name = '', $arguments = array() ) {
global $woocommerce_smart_coupon;
if ( ! is_callable( array( $woocommerce_smart_coupon, $function_name ) ) ) {
return;
}
if ( ! empty( $arguments ) ) {
return call_user_func_array( array( $woocommerce_smart_coupon, $function_name ), $arguments );
} else {
return call_user_func( array( $woocommerce_smart_coupon, $function_name ) );
}
}
/**
* Display field for coupon by excluded email
*
* @param array $args Arguments.
*/
public function usage_restriction( $args = array() ) {
$coupon_id = ( ! empty( $args['coupon_id'] ) ) ? absint( $args['coupon_id'] ) : 0;
$coupon = ( ! empty( $args['coupon_obj'] ) ) ? $args['coupon_obj'] : null;
$excluded_emails = ( is_object( $coupon ) && is_callable( array( $coupon, 'get_meta' ) ) ) ? $coupon->get_meta( 'wc_sc_excluded_customer_email' ) : get_post_meta( $coupon_id, 'wc_sc_excluded_customer_email', true );
if ( ! is_array( $excluded_emails ) || empty( $excluded_emails ) ) {
$excluded_emails = array();
}
woocommerce_wp_text_input(
array(
'id' => 'wc_sc_excluded_customer_email',
'label' => __( 'Excluded emails', 'woocommerce-smart-coupons' ),
'placeholder' => __( 'No restrictions', 'woocommerce-smart-coupons' ),
'description' => __( 'List of excluded billing emails to check against when an order is placed. Separate email addresses with commas. You can also use an asterisk (*) to match parts of an email. For example "*@gmail.com" would match all gmail addresses.', 'woocommerce-smart-coupons' ),
'value' => implode( ', ', $excluded_emails ),
'desc_tip' => true,
'type' => 'email',
'class' => '',
'custom_attributes' => array(
'multiple' => 'multiple',
),
)
);
}
/**
* Save coupon by excluded email data in meta
*
* @param Integer $post_id The coupon post ID.
* @param WC_Coupon $coupon The coupon object.
*/
public function process_meta( $post_id = 0, $coupon = null ) {
if ( empty( $post_id ) ) {
return;
}
$coupon = new WC_Coupon( $coupon );
$excluded_emails = ( isset( $_POST['wc_sc_excluded_customer_email'] ) ) ? wc_clean( wp_unslash( $_POST['wc_sc_excluded_customer_email'] ) ) : ''; // phpcs:ignore
$excluded_emails = explode( ',', $excluded_emails );
$excluded_emails = array_map( 'trim', $excluded_emails );
$excluded_emails = array_filter( $excluded_emails, 'is_email' );
$excluded_emails = array_filter( $excluded_emails );
if ( $this->is_callable( $coupon, 'update_meta_data' ) && $this->is_callable( $coupon, 'save' ) ) {
$coupon->update_meta_data( 'wc_sc_excluded_customer_email', $excluded_emails );
$coupon->save();
} else {
update_post_meta( $post_id, 'wc_sc_excluded_customer_email', $excluded_emails );
}
}
/**
* Validate the coupon based on user role
*
* @param boolean $valid Is valid or not.
* @param WC_Coupon $coupon The coupon object.
* @param WC_Discounts $discounts The discount object.
*
* @throws Exception If the coupon is invalid.
* @return boolean Is valid or not
*/
public function validate( $valid = false, $coupon = object, $discounts = null ) {
// If coupon is invalid already, no need for further checks.
if ( false === $valid ) {
return $valid;
}
$post_action = ( ! empty( $_POST['action'] ) ) ? wc_clean( wp_unslash( $_POST['action'] ) ) : ''; // phpcs:ignore
if ( is_admin() && wp_doing_ajax() && 'woocommerce_add_coupon_discount' === $post_action ) { // This condition will allow the addition of coupon from admin side, in the order even if the user role is not matching.
return true;
}
$is_auto_applied = did_action( 'wc_sc_before_auto_apply_coupons' ) > 0;
$cart = ( function_exists( 'WC' ) && isset( WC()->cart ) ) ? WC()->cart : null;
$coupon_id = ( $this->is_wc_gte_30() ) ? $coupon->get_id() : $coupon->id;
if ( ! is_a( $coupon, 'WC_Coupon' ) ) {
$coupon = new WC_Coupon( $coupon_id );
}
if ( $this->is_callable( $coupon, 'get_meta' ) ) {
$coupon_code = ( $this->is_callable( $coupon, 'get_code' ) ) ? $coupon->get_code() : '';
$customer_email = ( $this->is_callable( $coupon, 'get_email_restrictions' ) ) ? $coupon->get_email_restrictions() : array();
$exclude_customer_email = ( $this->is_callable( $coupon, 'get_meta' ) ) ? $coupon->get_meta( 'wc_sc_excluded_customer_email' ) : array();
} else {
$coupon_code = ( ! empty( $coupon->code ) ) ? $coupon->code : '';
$customer_email = get_post_meta( $coupon_id, 'customer_email', true );
$exclude_customer_email = get_post_meta( $coupon_id, 'wc_sc_excluded_customer_email', true );
}
$wc_customer = WC()->customer;
$wc_customer_email = ( $this->is_callable( $wc_customer, 'get_email' ) ) ? $wc_customer->get_email() : '';
$wc_customer_billing_email = ( $this->is_callable( $wc_customer, 'get_billing_email' ) ) ? $wc_customer->get_billing_email() : '';
$current_user = ( is_user_logged_in() ) ? wp_get_current_user() : null;
$user_email = ( ! is_null( $current_user ) && ! empty( $current_user->user_email ) ) ? $current_user->user_email : '';
$billing_email = ( ! empty( $_REQUEST['billing_email'] ) ) ? wc_clean( wp_unslash( $_REQUEST['billing_email'] ) ) : ''; // phpcs:ignore
$check_emails = array_unique(
array_filter(
array_map(
'strtolower',
array_map(
'sanitize_email',
array(
$billing_email,
$wc_customer_billing_email,
$wc_customer_email,
$user_email,
)
)
)
)
);
if ( false === $is_auto_applied && empty( $check_emails ) ) {
return $valid;
}
$is_callable_coupon_emails_allowed = $this->is_callable( $cart, 'is_coupon_emails_allowed' );
$is_validate_allowed_emails = apply_filters(
'wc_sc_force_validate_allowed_emails',
true,
array(
'source' => $this,
'is_valid' => $valid,
'coupon_obj' => $coupon,
'discounts_obj' => $discounts,
)
);
if ( true === $is_validate_allowed_emails && is_array( $customer_email ) && 0 < count( $customer_email ) && true === $is_callable_coupon_emails_allowed && ! $cart->is_coupon_emails_allowed( $check_emails, $customer_email ) ) {
if ( true === $is_auto_applied ) {
$valid = false;
if ( $this->is_callable( $cart, 'has_discount' ) && $cart->has_discount( $coupon_code ) && $this->is_callable( $cart, 'remove_coupon' ) ) {
if ( class_exists( 'WC_Coupon' ) && $this->is_callable( $coupon, 'add_coupon_message' ) ) {
$coupon->add_coupon_message( WC_Coupon::E_WC_COUPON_NOT_YOURS_REMOVED );
}
$cart->remove_coupon( $coupon_code );
}
} else {
throw new Exception( __( 'This coupon is not valid for you.', 'woocommerce-smart-coupons' ) );
}
}
if ( is_array( $exclude_customer_email ) && 0 < count( $exclude_customer_email ) && true === $is_callable_coupon_emails_allowed && $cart->is_coupon_emails_allowed( $check_emails, $exclude_customer_email ) ) {
if ( true === $is_auto_applied ) {
$valid = false;
if ( $this->is_callable( $cart, 'has_discount' ) && $cart->has_discount( $coupon_code ) && $this->is_callable( $cart, 'remove_coupon' ) ) {
if ( class_exists( 'WC_Coupon' ) && $this->is_callable( $coupon, 'add_coupon_message' ) ) {
$coupon->add_coupon_message( WC_Coupon::E_WC_COUPON_NOT_YOURS_REMOVED );
}
$cart->remove_coupon( $coupon_code );
}
} else {
throw new Exception( __( 'This coupon is not valid for you.', 'woocommerce-smart-coupons' ) );
}
}
return $valid;
}
/**
* Now that we have billing email, check if the coupon is excluded to be used for the user or billing email
*
* Credit: WooCommerce
*
* @param array $posted Post data.
*/
public function check_customer_coupons( $posted = array() ) {
$cart = ( function_exists( 'WC' ) && isset( WC()->cart ) ) ? WC()->cart : null;
if ( is_a( $cart, 'WC_Cart' ) ) {
$is_cart_empty = is_callable( array( $cart, 'is_empty' ) ) && $cart->is_empty();
if ( false === $is_cart_empty ) {
$applied_coupons = ( is_callable( array( $cart, 'get_applied_coupons' ) ) ) ? $cart->get_applied_coupons() : array();
if ( ! empty( $applied_coupons ) ) {
foreach ( $applied_coupons as $code ) {
$coupon = new WC_Coupon( $code );
if ( $this->is_valid( $coupon ) ) {
// Get user and posted emails to compare.
$current_user = ( is_user_logged_in() ) ? wp_get_current_user() : null;
$user_email = ( ! is_null( $current_user ) && ! empty( $current_user->user_email ) ) ? $current_user->user_email : '';
$billing_email = isset( $posted['billing_email'] ) ? $posted['billing_email'] : '';
$check_emails = array_unique(
array_filter(
array_map(
'strtolower',
array_map(
'sanitize_email',
array(
$billing_email,
$user_email,
)
)
)
)
);
if ( is_object( $coupon ) && is_callable( array( $coupon, 'get_meta' ) ) ) {
$exclude_restrictions = $coupon->get_meta( 'wc_sc_excluded_customer_email' );
} else {
if ( is_object( $coupon ) && is_callable( array( $coupon, 'get_id' ) ) ) {
$coupon_id = $coupon->get_id();
} else {
$coupon_id = ( ! empty( $coupon->id ) ) ? $coupon->id : 0;
}
$exclude_restrictions = ( ! empty( $coupon_id ) ) ? get_post_meta( $coupon_id, 'wc_sc_excluded_customer_email', true ) : array();
}
if ( is_array( $exclude_restrictions ) && 0 < count( $exclude_restrictions ) && is_callable( array( $coupon, 'add_coupon_message' ) ) && is_callable( array( $cart, 'remove_coupon' ) ) && is_callable( array( $cart, 'is_coupon_emails_allowed' ) ) && $cart->is_coupon_emails_allowed( $check_emails, $exclude_restrictions ) ) {
$coupon->add_coupon_message( WC_Coupon::E_WC_COUPON_NOT_YOURS_REMOVED );
$cart->remove_coupon( $code );
}
/*
|===========================================================================================================================================================================|
| |
| Before this method, WooCommerce checks for Allowed emails. |
| And in that method, it already checks for the usage limit whether it is allowed to apply the coupon or not. |
| 1. If it's allowed, it means the usage limit is within reach & we can proceed with checking for excluded email. |
| Because the main purpose of excluded email is to prevent application of coupon. And since the usage limit is already checked, it's not needed to check it again |
| 2. If it's not allowed, the process will not reach in this method, as it's already invalidated. |
| |
|===========================================================================================================================================================================|
*/
}
}
}
}
}
}
/**
* Add meta in export headers
*
* @param array $headers Existing headers.
* @return array
*/
public function export_headers( $headers = array() ) {
$headers['wc_sc_excluded_customer_email'] = __( 'Excluded emails', 'woocommerce-smart-coupons' );
return $headers;
}
/**
* Post meta defaults for excluded email ids meta
*
* @param array $defaults Existing postmeta defaults.
* @return array $defaults Modified postmeta defaults
*/
public function postmeta_defaults( $defaults = array() ) {
$defaults['wc_sc_excluded_customer_email'] = '';
return $defaults;
}
/**
* Make meta data of excluded email ids protected
*
* @param bool $protected Is protected.
* @param string $meta_key The meta key.
* @param string $meta_type The meta type.
* @return bool $protected
*/
public function make_action_meta_protected( $protected = false, $meta_key = '', $meta_type = '' ) {
$sc_excluded_email_keys = array(
'wc_sc_excluded_customer_email',
);
if ( in_array( $meta_key, $sc_excluded_email_keys, true ) ) {
return true;
}
return $protected;
}
/**
* Add excluded email in coupon meta
*
* @param array $data The row data.
* @param array $post The POST values.
* @return array Modified data
*/
public function generate_coupon_meta( $data = array(), $post = array() ) {
$excluded_emails = ( isset( $post['wc_sc_excluded_customer_email'] ) ) ? wc_clean( wp_unslash( $post['wc_sc_excluded_customer_email'] ) ) : ''; // phpcs:ignore
$excluded_emails = explode( ',', $excluded_emails );
$excluded_emails = array_map( 'trim', $excluded_emails );
$excluded_emails = array_filter( $excluded_emails, 'is_email' );
$excluded_emails = array_filter( $excluded_emails );
$excluded_emails = implode( ',', $excluded_emails );
$data['wc_sc_excluded_customer_email'] = $excluded_emails;
return $data;
}
/**
* Process coupon meta value for import
*
* @param mixed $meta_value The meta value.
* @param array $args Additional Arguments.
* @return mixed $meta_value
*/
public function import_coupon_meta( $meta_value = null, $args = array() ) {
if ( ! empty( $args['meta_key'] ) && 'wc_sc_excluded_customer_email' === $args['meta_key'] ) {
$excluded_emails = ( isset( $args['postmeta']['wc_sc_excluded_customer_email'] ) ) ? wc_clean( wp_unslash( $args['postmeta']['wc_sc_excluded_customer_email'] ) ) : ''; // phpcs:ignore
$excluded_emails = explode( ',', $excluded_emails );
$excluded_emails = array_map( 'trim', $excluded_emails );
$excluded_emails = array_filter( $excluded_emails, 'is_email' );
$meta_value = array_filter( $excluded_emails );
}
return $meta_value;
}
/**
* Function to copy excluded email meta in newly generated coupon
*
* @param array $args The arguments.
*/
public function copy_coupon_meta( $args = array() ) {
$new_coupon_id = ( ! empty( $args['new_coupon_id'] ) ) ? absint( $args['new_coupon_id'] ) : 0;
$coupon = ( ! empty( $args['ref_coupon'] ) ) ? $args['ref_coupon'] : false;
if ( empty( $new_coupon_id ) || empty( $coupon ) ) {
return;
}
if ( $this->is_wc_gte_30() && is_object( $coupon ) && is_callable( array( $coupon, 'get_meta' ) ) ) {
$excluded_emails = $coupon->get_meta( 'wc_sc_excluded_customer_email' );
} else {
$old_coupon_id = ( ! empty( $coupon->id ) ) ? $coupon->id : 0;
$excluded_emails = get_post_meta( $old_coupon_id, 'wc_sc_excluded_customer_email', true );
}
if ( ! is_array( $excluded_emails ) || empty( $excluded_emails ) ) {
$excluded_emails = array();
}
$new_coupon = new WC_Coupon( $new_coupon_id );
if ( is_object( $new_coupon ) && is_callable( array( $new_coupon, 'update_meta_data' ) ) && is_callable( array( $new_coupon, 'save' ) ) ) {
$new_coupon->update_meta_data( 'wc_sc_excluded_customer_email', $excluded_emails );
$new_coupon->save();
} else {
update_post_meta( $new_coupon_id, 'wc_sc_excluded_customer_email', $excluded_emails );
}
}
/**
* Define which columns to show on this screen.
*
* @param array $columns Existing columns.
* @return array
*/
public function define_columns( $columns = array() ) {
if ( ! is_array( $columns ) || empty( $columns ) ) {
$columns = array();
}
$columns['wc_sc_coupon_allowed_emails'] = __( 'Allowed emails', 'woocommerce-smart-coupons' );
$columns['wc_sc_coupon_excluded_emails'] = __( 'Excluded emails', 'woocommerce-smart-coupons' );
return $columns;
}
/**
* Render individual columns.
*
* @param string $column Column ID to render.
* @param int $post_id Post ID being shown.
*/
public function render_columns( $column = '', $post_id = 0 ) {
if ( empty( $post_id ) || empty( $column ) || ! in_array( $column, array( 'wc_sc_coupon_allowed_emails', 'wc_sc_coupon_excluded_emails' ), true ) ) {
return;
}
$coupon = new WC_Coupon( $post_id );
switch ( $column ) {
case 'wc_sc_coupon_allowed_emails':
$this->render_allowed_emails_column( $post_id, $coupon );
break;
case 'wc_sc_coupon_excluded_emails':
$this->render_excluded_emails_column( $post_id, $coupon );
break;
}
}
/**
* Function to render allowed emails column on coupons dashboard.
*
* @param int $post_id The coupon ID.
* @param WC_Coupon $coupon The coupon object.
*/
public function render_allowed_emails_column( $post_id = 0, $coupon = null ) {
$emails = ( $this->is_callable( $coupon, 'get_email_restrictions' ) ) ? $coupon->get_email_restrictions() : $this->get_post_meta( $post_id, 'customer_email', true );
echo wp_kses_post(
$this->email_column_value(
$emails,
array(
'coupon_id' => $post_id,
'coupon_obj' => $coupon,
'filter' => true, // Allow filtering coupon list by selected "allowed email".
)
)
);
}
/**
* Function to render excluded emails column on coupons dashboard.
*
* @param int $post_id The coupon ID.
* @param WC_Coupon $coupon The coupon object.
*/
public function render_excluded_emails_column( $post_id = 0, $coupon = null ) {
$emails = ( $this->is_callable( $coupon, 'get_meta' ) ) ? $coupon->get_meta( 'wc_sc_excluded_customer_email' ) : $this->get_post_meta( $post_id, 'wc_sc_excluded_customer_email', true );
echo wp_kses_post(
$this->email_column_value(
$emails,
array(
'coupon_id' => $post_id,
'coupon_obj' => $coupon,
)
)
);
}
/**
* Get value for email columns.
*
* @param array $emails The list of email addresses.
* @param array $args Additional arguments.
* @return string $value
*/
public function email_column_value( $emails = array(), $args = array() ) {
$coupon_id = ( ! empty( $args['coupon_id'] ) ) ? absint( $args['coupon_id'] ) : 0;
$coupon = ( ! empty( $args['coupon_obj'] ) ) ? $args['coupon_obj'] : null;
$filter = ( ! empty( $args['filter'] ) ) ? $args['filter'] : false;
$value = '<span class="na">&ndash;</span>';
if ( ! empty( $emails ) ) {
$email_count = apply_filters(
'wc_sc_email_column_max_count',
$this->sc_get_option( 'wc_sc_email_column_max_count', 5 ),
array(
'source' => $this,
'coupon_id' => $coupon_id,
'coupon_obj' => $coupon,
'filter' => $filter,
)
);
$visible_emails = ( ! empty( $email_count ) && is_array( $emails ) ) ? array_slice( $emails, 0, $email_count ) : array();
if ( ! empty( $visible_emails ) ) {
$mapped_emails = ( true === $filter ) ? array_map( array( $this, 'filter_by_email_link' ), $visible_emails ) : $visible_emails;
$value = implode( ', ', $mapped_emails );
}
}
return $value;
}
}
}
WC_SC_Coupons_By_Excluded_Email::get_instance();

View File

@@ -0,0 +1,710 @@
<?php
/**
* Class to handle feature Coupons By Location
*
* @author StoreApps
* @category Admin
* @package wocommerce-smart-coupons/includes
* @version 1.9.0
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
if ( ! class_exists( 'WC_SC_Coupons_By_Location' ) ) {
/**
* Class WC_SC_Coupons_By_Location
*/
class WC_SC_Coupons_By_Location {
/**
* Variable to hold instance of this class
*
* @var $instance
*/
private static $instance = null;
/**
* Additional Locations
*
* @var $additional_locations
*/
public $additional_locations = array();
/**
* Countries
*
* @var $countries
*/
public $countries;
/**
* Global Additional Locations
*
* @var $global_additional_locations
*/
public $global_additional_locations = array();
/**
* Custom Locations
*
* @var $custom_locations
*/
public $custom_locations = array();
/**
* Locations Lookup In
*
* @var $locations_lookup_in
*/
public $locations_lookup_in;
/**
* Address
*
* @var $address
*/
public $address;
/**
* Constructor
*/
private function __construct() {
add_action( 'init', array( $this, 'initialize_cbl_additional_locations' ) );
add_filter( 'woocommerce_coupon_is_valid', array( $this, 'validate' ), 11, 3 );
if ( is_admin() ) {
add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_admin_javascript_css' ) );
add_action( 'woocommerce_coupon_options_usage_restriction', array( $this, 'usage_restriction' ) );
add_action( 'woocommerce_coupon_options_save', array( $this, 'process_meta' ), 10, 2 );
}
add_filter( 'wc_smart_coupons_export_headers', array( $this, 'export_headers' ) );
add_filter( 'wc_sc_export_coupon_meta_data', array( $this, 'export_coupon_meta_data' ), 10, 2 );
add_filter( 'smart_coupons_parser_postmeta_defaults', array( $this, 'postmeta_defaults' ) );
add_filter( 'sc_generate_coupon_meta', array( $this, 'generate_coupon_meta' ), 10, 2 );
add_filter( 'is_protected_meta', array( $this, 'make_action_meta_protected' ), 10, 3 );
add_filter( 'wc_sc_process_coupon_meta_value_for_import', array( $this, 'process_coupon_meta_value_for_import' ), 10, 2 );
add_action( 'wc_sc_new_coupon_generated', array( $this, 'copy_coupon_action_meta' ) );
}
/**
* Get single instance of this class
*
* @return this class Singleton object of this class
*/
public static function get_instance() {
// Check if instance is already exists.
if ( is_null( self::$instance ) ) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Handle call to functions which is not available in this class
*
* @param string $function_name The function name.
* @param array $arguments Array of arguments passed while calling $function_name.
* @return result of function call
*/
public function __call( $function_name, $arguments = array() ) {
global $woocommerce_smart_coupon;
if ( ! is_callable( array( $woocommerce_smart_coupon, $function_name ) ) ) {
return;
}
if ( ! empty( $arguments ) ) {
return call_user_func_array( array( $woocommerce_smart_coupon, $function_name ), $arguments );
} else {
return call_user_func( array( $woocommerce_smart_coupon, $function_name ) );
}
}
/**
* Initialize additional locations
*/
public function initialize_cbl_additional_locations() {
if ( empty( $this->global_additional_locations ) ) {
$this->global_additional_locations = get_option( 'sa_cbl_additional_locations', array() );
}
}
/**
* Styles & scripts
*/
public function enqueue_admin_javascript_css() {
global $woocommerce_smart_coupon;
$post_type = $this->get_post_type();
$get_page = ( ! empty( $_GET['page'] ) ) ? wc_clean( wp_unslash( $_GET['page'] ) ) : ''; // phpcs:ignore
if ( 'shop_coupon' !== $post_type && 'wc-smart-coupons' !== $get_page ) {
return;
}
$suffix = defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ? '' : '.min';
// Add customized chosen javascript library for adding new location from same field.
wp_register_script( 'sa_coupons_by_location_chosen', plugins_url( '/', dirname( __FILE__ ) ) . 'assets/js/chosen.jquery' . $suffix . '.js', array( 'jquery' ), $woocommerce_smart_coupon->plugin_data['Version'], true );
wp_enqueue_style( 'sa_coupons_by_location_css', plugins_url( '/', dirname( __FILE__ ) ) . 'assets/css/cbl-admin' . $suffix . '.css', array(), $woocommerce_smart_coupon->plugin_data['Version'] );
}
/**
* Display field for coupon by location
*/
public function usage_restriction() {
global $post;
if ( ! wp_script_is( 'sa_coupons_by_location_chosen' ) ) {
wp_enqueue_script( 'sa_coupons_by_location_chosen' );
}
$coupon = ( ! empty( $post->ID ) ) ? new WC_Coupon( $post->ID ) : null;
$is_callable_coupon_get_meta = $this->is_callable( $coupon, 'get_meta' );
$this->locations_lookup_in = ( true === $is_callable_coupon_get_meta ) ? $coupon->get_meta( 'sa_cbl_locations_lookup_in' ) : get_post_meta( $post->ID, 'sa_cbl_locations_lookup_in', true );
if ( ! is_array( $this->locations_lookup_in ) || empty( $this->locations_lookup_in ) ) {
$this->locations_lookup_in = array( 'address' => 'billing' );
$this->update_post_meta( $post->ID, 'sa_cbl_locations_lookup_in', $this->locations_lookup_in );
}
$this->address = $this->locations_lookup_in['address'];
$is_wc_countries = function_exists( 'WC' ) && is_object( WC()->countries );
$continents = ( $is_wc_countries && $this->is_callable( WC()->countries, 'get_continents' ) ) ? WC()->countries->get_continents() : array();
$countries = ( $is_wc_countries && $this->is_callable( WC()->countries, 'get_countries' ) ) ? WC()->countries->get_countries() : array();
$all_states = ( $is_wc_countries && $this->is_callable( WC()->countries, 'get_states' ) ) ? WC()->countries->get_states() : array();
?>
<div class="options_group smart-coupons-field" id="locations">
<p class="form-field">
<span class='search_in'><label><?php echo esc_html__( 'Address to look in', 'woocommerce-smart-coupons' ); ?></label></span>
<label for="billing" class="billing">
<input type="radio" name="sa_cbl_search_in[address]" value="billing" <?php ( ! empty( $this->address ) && 'billing' === $this->address ) ? checked( $this->address, 'billing' ) : ''; ?> />
<?php echo esc_html__( 'Billing', 'woocommerce-smart-coupons' ); ?>
</label> &nbsp;
<label for="shipping" class="shipping">
<input type="radio" name="sa_cbl_search_in[address]" value="shipping" <?php ( ! empty( $this->address ) && 'shipping' === $this->address ) ? checked( $this->address, 'shipping' ) : ''; ?> />
<?php echo esc_html__( 'Shipping', 'woocommerce-smart-coupons' ); ?>
</label>
</p>
<p class="form-field">
<label class="options_header"><?php echo esc_html__( 'Locations', 'woocommerce-smart-coupons' ); ?></label>
<?php
$locations = ( true === $is_callable_coupon_get_meta ) ? $coupon->get_meta( 'sa_cbl_' . $this->address . '_locations' ) : get_post_meta( $post->ID, 'sa_cbl_' . $this->address . '_locations', true );
if ( empty( $locations ) || ! is_array( $locations ) ) {
$locations = array();
}
if ( ! array_key_exists( 'additional_locations', $locations ) || ! is_array( $locations['additional_locations'] ) ) {
$locations['additional_locations'] = array();
}
$this->additional_locations = array_map( 'html_entity_decode', $locations['additional_locations'] );
$this->countries = WC()->countries->countries;
echo '<select name="locations[additional_locations][]" id="cc_list" data-placeholder="' . esc_html__( 'Select location', 'woocommerce-smart-coupons' ) . '..." class="sa_cbl_search_location sa_cbl_add_location" multiple>';
if ( ! empty( $continents ) ) {
foreach ( $continents as $continent_code => $continent ) {
?>
<optgroup label="<?php echo esc_attr( ( ! empty( $continent['name'] ) ) ? $continent['name'] : $continent_code ); ?>">
<?php
if ( ! empty( $continent['countries'] ) ) {
foreach ( $continent['countries'] as $country_code ) {
if ( array_key_exists( $country_code, $countries ) && ! empty( $countries[ $country_code ] ) ) {
?>
<option value="<?php echo esc_attr( $country_code ); ?>"
<?php
if ( ! empty( $this->additional_locations ) ) {
$encoding = $this->mb_detect_encoding( $country_code, 'UTF-8, ISO-8859-1', true );
$decoded_country_code = ( false !== $encoding ) ? html_entity_decode( $country_code, ENT_COMPAT, $encoding ) : $country_code;
echo esc_attr( selected( in_array( $decoded_country_code, $this->additional_locations, true ) ) );
}
?>
><?php echo esc_html( ( ! empty( $countries[ $country_code ] ) ) ? trim( $countries[ $country_code ] ) : trim( $country_code ) ); ?>
</option>
<?php
}
}
}
?>
</optgroup>
<?php
}
}
if ( ! empty( $all_states ) ) {
foreach ( $all_states as $country_code => $states ) {
if ( empty( $states ) ) {
continue;
}
?>
<optgroup label="<?php echo esc_attr( ( ! empty( $countries[ $country_code ] ) ) ? $countries[ $country_code ] : $country_code ); ?>">
<?php
if ( ! empty( $states ) ) {
foreach ( $states as $state_code => $state_name ) {
$country_state_code = $country_code . '::' . $state_code;
?>
<option value="<?php echo esc_attr( $country_state_code ); ?>"
<?php
if ( ! empty( $this->additional_locations ) ) {
$encoding = $this->mb_detect_encoding( $country_state_code, 'UTF-8, ISO-8859-1', true );
$decoded_country_state_code = ( false !== $encoding ) ? html_entity_decode( $country_state_code, ENT_COMPAT, $encoding ) : $country_state_code;
echo esc_attr( selected( in_array( $decoded_country_state_code, $this->additional_locations, true ) ) );
}
?>
><?php echo esc_html( trim( $state_name ) ); ?>
</option>
<?php
}
}
?>
</optgroup>
<?php
}
}
// others.
echo ' <optgroup label="' . esc_html__( 'Select Additional Locations', 'woocommerce-smart-coupons' ) . '"> ';
if ( ! empty( $this->global_additional_locations ) ) {
foreach ( $this->global_additional_locations as $list ) {
echo '<option value="' . esc_attr( $list ) . '"';
if ( ! empty( $this->additional_locations ) ) {
$encoding = $this->mb_detect_encoding( $list, 'UTF-8, ISO-8859-1', true );
$decoded_list = ( false !== $encoding ) ? html_entity_decode( $list, ENT_COMPAT, $encoding ) : $list;
echo esc_attr( selected( in_array( $decoded_list, $this->additional_locations, true ) ) );
}
echo '>' . esc_html( ucwords( strtolower( $list ) ) ) . '</option>';
}
}
echo ' </optgroup> ';
echo '</select>';
?>
</p>
</div>
<?php
$js = "jQuery('select.sa_cbl_search_location').chosen({
disable_search_threshold: 10,
width: '50%',
no_results_text: '" . __( 'Entered location not found. On pressing "Enter" button, a new custom location will be saved as: ', 'woocommerce-smart-coupons' ) . "'
});";
$js .= "jQuery('#cc_list_chosen').on('click', function(){
var cc_height = jQuery('#cc_list_chosen').height();
jQuery('#cc_list_chosen .chosen-drop').attr('style', 'bottom: '+cc_height+'px !important; border-bottom: 0 !important; border-top: 1px solid #aaa !important; top: auto !important;');
});
";
wc_enqueue_js( $js );
}
/**
* Save coupon by location data in meta
*
* @param Integer $post_id The coupon post ID.
* @param WC_Coupon $coupon The coupon object.
*/
public function process_meta( $post_id = 0, $coupon = null ) {
if ( empty( $post_id ) ) {
return;
}
$coupon = new WC_Coupon( $coupon );
$locations = ( ! empty( $_POST['locations'] ) ) ? wc_clean( wp_unslash( $_POST['locations'] ) ) : array(); // phpcs:ignore
$this->locations_lookup_in['address'] = ( ! empty( $_POST['sa_cbl_search_in']['address'] ) ) ? wc_clean( wp_unslash( $_POST['sa_cbl_search_in']['address'] ) ) : ''; // phpcs:ignore
if ( $this->is_callable( $coupon, 'update_meta_data' ) && $this->is_callable( $coupon, 'save' ) ) {
$coupon->update_meta_data( 'sa_cbl_' . $this->locations_lookup_in['address'] . '_locations', $locations );
if ( isset( $this->locations_lookup_in['address'] ) && ! empty( $this->locations_lookup_in['address'] ) ) {
$coupon->update_meta_data( 'sa_cbl_locations_lookup_in', $this->locations_lookup_in );
}
$coupon->save();
} else {
update_post_meta( $post_id, 'sa_cbl_' . $this->locations_lookup_in['address'] . '_locations', $locations );
if ( isset( $this->locations_lookup_in['address'] ) && ! empty( $this->locations_lookup_in['address'] ) ) {
update_post_meta( $post_id, 'sa_cbl_locations_lookup_in', $this->locations_lookup_in );
}
}
$this->countries = array_map( 'strtolower', WC()->countries->countries );
if ( ! empty( $locations['additional_locations'] ) ) {
$this->additional_locations = array_map( 'strtolower', $locations['additional_locations'] );
}
if ( count( $this->additional_locations ) > 0 ) {
// Loop through all location entered in Billing location of coupons.
// and collect those location which is not available in WooCommerce countries.
foreach ( $this->additional_locations as $location ) {
if ( ! in_array( $location, $this->countries, true ) ) {
$this->custom_locations[] = strtolower( $location );
}
}
// Add new location with already saved locations.
if ( false !== $this->global_additional_locations && ! empty( $this->global_additional_locations ) ) {
$this->global_additional_locations = array_merge( $this->global_additional_locations, $this->custom_locations );
} else {
$this->global_additional_locations = $this->custom_locations;
}
// Discard duplicate values, arrange alphabetically & save.
$this->global_additional_locations = array_unique( $this->global_additional_locations );
sort( $this->global_additional_locations );
update_option( 'sa_cbl_additional_locations', $this->global_additional_locations, 'no' );
}
}
/**
* Validate the coupon based on location
*
* @param boolean $valid Is valid or not.
* @param WC_Coupon $coupon The coupon object.
* @param WC_Discounts $discounts The discount object.
*
* @throws Exception If the coupon is invalid.
* @return boolean Is valid or not
*/
public function validate( $valid, $coupon, $discounts = null ) {
global $checkout;
// If coupon is invalid already, no need for further checks.
if ( ! $valid ) {
return $valid;
}
$coupon_id = ( $this->is_wc_gte_30() ) ? $coupon->get_id() : $coupon->id;
$is_callable_coupon_get_meta = $this->is_callable( $coupon, 'get_meta' );
$this->locations_lookup_in = ( true === $is_callable_coupon_get_meta ) ? $coupon->get_meta( 'sa_cbl_locations_lookup_in' ) : get_post_meta( $coupon_id, 'sa_cbl_locations_lookup_in', true );
if ( empty( $this->locations_lookup_in ) || empty( $this->locations_lookup_in['address'] ) ) {
return $valid;
}
$locations = ( true === $is_callable_coupon_get_meta ) ? $coupon->get_meta( 'sa_cbl_' . $this->locations_lookup_in['address'] . '_locations' ) : get_post_meta( $coupon_id, 'sa_cbl_' . $this->locations_lookup_in['address'] . '_locations', true );
if ( ! empty( $locations ) && is_array( $locations ) && ! empty( $locations['additional_locations'] ) && is_array( $locations['additional_locations'] ) && array_key_exists( 'additional_locations', $locations ) ) {
$wc_customer = WC()->customer;
$wc_countries = WC()->countries;
$check_live_details = false;
$wc_ajax_action = ( ! empty( $_REQUEST['wc-ajax'] ) ) ? wc_clean( wp_unslash( $_REQUEST['wc-ajax'] ) ) : ''; // phpcs:ignore
if ( defined( 'WC_DOING_AJAX' ) && WC_DOING_AJAX && 'update_order_review' === $wc_ajax_action ) {
check_ajax_referer( 'update-order-review', 'security' );
$check_live_details = true;
}
// Collect country, state & city.
if ( 'billing' === $this->locations_lookup_in['address'] ) {
if ( $this->is_wc_gte_30() ) {
$customer_billing_country = ( true === $check_live_details && ! empty( $_REQUEST['country'] ) ) ? wc_clean( wp_unslash( $_REQUEST['country'] ) ) : $wc_customer->get_billing_country(); // phpcs:ignore
$customer_billing_state = ( true === $check_live_details && ! empty( $_REQUEST['state'] ) ) ? wc_clean( wp_unslash( $_REQUEST['state'] ) ) : $wc_customer->get_billing_state(); // phpcs:ignore
$customer_billing_city = ( true === $check_live_details && ! empty( $_REQUEST['city'] ) ) ? wc_clean( wp_unslash( $_REQUEST['city'] ) ) : $wc_customer->get_billing_city(); // phpcs:ignore
$customer_billing_postcode = ( true === $check_live_details && ! empty( $_REQUEST['postcode'] ) ) ? wc_clean( wp_unslash( $_REQUEST['postcode'] ) ) : $wc_customer->get_billing_postcode(); // phpcs:ignore
$current_country = ( ! empty( $customer_billing_country ) ) ? $customer_billing_country : '';
$current_state = ( ! empty( $customer_billing_state ) ) ? $customer_billing_state : '';
$current_city = ( ! empty( $customer_billing_city ) ) ? $customer_billing_city : '';
$current_post_code = ( ! empty( $customer_billing_postcode ) ) ? $customer_billing_postcode : '';
} else {
$current_country = ( ! empty( $wc_customer->country ) ) ? $wc_customer->country : '';
$current_state = ( ! empty( $wc_customer->state ) ) ? $wc_customer->state : '';
$current_city = ( ! empty( $wc_customer->city ) ) ? $wc_customer->city : '';
$current_post_code = ( ! empty( $wc_customer->postcode ) ) ? $wc_customer->postcode : '';
}
} else {
if ( $this->is_wc_gte_30() ) {
$customer_shipping_country = ( true === $check_live_details && ! empty( $_REQUEST['s_country'] ) ) ? wc_clean( wp_unslash( $_REQUEST['s_country'] ) ) : $wc_customer->get_shipping_country(); // phpcs:ignore
$customer_shipping_state = ( true === $check_live_details && ! empty( $_REQUEST['s_state'] ) ) ? wc_clean( wp_unslash( $_REQUEST['s_state'] ) ) : $wc_customer->get_shipping_state(); // phpcs:ignore
$customer_shipping_city = ( true === $check_live_details && ! empty( $_REQUEST['s_city'] ) ) ? wc_clean( wp_unslash( $_REQUEST['s_city'] ) ) : $wc_customer->get_shipping_city(); // phpcs:ignore
$customer_shipping_postcode = ( true === $check_live_details && ! empty( $_REQUEST['s_postcode'] ) ) ? wc_clean( wp_unslash( $_REQUEST['s_postcode'] ) ) : $wc_customer->get_shipping_postcode(); // phpcs:ignore
$current_country = ( ! empty( $customer_shipping_country ) ) ? $customer_shipping_country : '';
$current_state = ( ! empty( $customer_shipping_state ) ) ? $customer_shipping_state : '';
$current_city = ( ! empty( $customer_shipping_city ) ) ? $customer_shipping_city : '';
$current_post_code = ( ! empty( $customer_shipping_postcode ) ) ? $customer_shipping_postcode : '';
} else {
$current_country = ( ! empty( $wc_customer->shipping_country ) ) ? $wc_customer->shipping_country : '';
$current_state = ( ! empty( $wc_customer->shipping_state ) ) ? $wc_customer->shipping_state : '';
$current_city = ( ! empty( $wc_customer->shipping_city ) ) ? $wc_customer->shipping_city : '';
$current_post_code = ( ! empty( $wc_customer->shipping_postcode ) ) ? $wc_customer->shipping_postcode : '';
}
}
// Convert country code or state code to actual country & state.
$country = ( ! empty( $wc_countries->countries[ $current_country ] ) ) ? strtolower( $wc_countries->countries[ $current_country ] ) : '';
$state = ( ! empty( $wc_countries->states[ $current_country ][ $current_state ] ) ) ? strtolower( $wc_countries->states[ $current_country ][ $current_state ] ) : strtolower( $current_state );
$city = ( ! empty( $current_city ) ) ? strtolower( $current_city ) : '';
$post_code = ( ! empty( $current_post_code ) ) ? strtolower( $current_post_code ) : '';
// Loop through additional_locations and return true on matching with either country, state or city.
// Return false otherwise.
foreach ( $locations['additional_locations'] as $additional_location ) {
if ( false !== strpos( $additional_location, '::' ) ) {
$exploded_location = explode( '::', $additional_location );
$additional_location_country = ( ! empty( $exploded_location[0] ) ) ? $exploded_location[0] : '';
$additional_location_state = ( ! empty( $exploded_location[1] ) ) ? $exploded_location[1] : '';
if ( $current_country === $additional_location_country && $current_state === $additional_location_state ) {
return true;
}
}
if ( $current_country === $additional_location || $current_state === $additional_location || $country === $additional_location || $state === $additional_location || $city === $additional_location || $post_code === $additional_location ) {
return true;
}
}
throw new Exception( __( 'Coupon is not valid for the', 'woocommerce-smart-coupons' ) . ' ' . ( ( 'billing' === $this->locations_lookup_in['address'] ) ? __( 'billing address', 'woocommerce-smart-coupons' ) : __( 'shipping address', 'woocommerce-smart-coupons' ) ) );
}
return $valid;
}
/**
* Add meta in export headers
*
* @param array $headers Existing headers.
* @return array
*/
public function export_headers( $headers = array() ) {
$cbl_headers = array(
'sa_cbl_locations_lookup_in' => __( 'Locations lookup in', 'woocommerce-smart-coupons' ),
'sa_cbl_billing_locations' => __( 'Billing Locations', 'woocommerce-smart-coupons' ),
'sa_cbl_shipping_locations' => __( 'Shipping Locations', 'woocommerce-smart-coupons' ),
);
return array_merge( $headers, $cbl_headers );
}
/**
* Function to handle coupon meta data during export of existing coupons
*
* @param mixed $meta_value The meta value.
* @param array $args Additional arguments.
* @return string Processed meta value
*/
public function export_coupon_meta_data( $meta_value = '', $args = array() ) {
if ( ! empty( $args['meta_key'] ) && in_array( $args['meta_key'], array( 'sa_cbl_billing_locations', 'sa_cbl_shipping_locations' ), true ) ) {
switch ( $args['meta_key'] ) {
case 'sa_cbl_billing_locations':
case 'sa_cbl_shipping_locations':
$meta_value = ( ! empty( $meta_value['additional_locations'] ) ) ? implode( '|', wc_clean( wp_unslash( $meta_value['additional_locations'] ) ) ) : '';
break;
}
}
return $meta_value;
}
/**
* Post meta defaults for CBL's meta
*
* @param array $defaults Existing postmeta defaults.
* @return array
*/
public function postmeta_defaults( $defaults = array() ) {
$cbl_defaults = array(
'sa_cbl_locations_lookup_in' => '',
'sa_cbl_billing_locations' => '',
'sa_cbl_shipping_locations' => '',
);
return array_merge( $defaults, $cbl_defaults );
}
/**
* Add CBL's meta with value in coupon meta
*
* @param array $data The row data.
* @param array $post The POST values.
* @return array Modified data
*/
public function generate_coupon_meta( $data = array(), $post = array() ) {
$data['sa_cbl_locations_lookup_in'] = ( ! empty( $post['sa_cbl_search_in']['address'] ) ) ? wc_clean( wp_unslash( $post['sa_cbl_search_in']['address'] ) ) : '';
$data['sa_cbl_billing_locations'] = ( ! empty( $data['sa_cbl_locations_lookup_in'] ) && 'billing' === $data['sa_cbl_locations_lookup_in'] && ! empty( $post['locations']['additional_locations'] ) && is_array( $post['locations']['additional_locations'] ) ) ? implode( '|', wc_clean( wp_unslash( $post['locations']['additional_locations'] ) ) ) : '';
$data['sa_cbl_shipping_locations'] = ( ! empty( $data['sa_cbl_locations_lookup_in'] ) && 'shipping' === $data['sa_cbl_locations_lookup_in'] && ! empty( $post['locations']['additional_locations'] ) && is_array( $post['locations']['additional_locations'] ) ) ? implode( '|', wc_clean( wp_unslash( $post['locations']['additional_locations'] ) ) ) : '';
if ( ! empty( $post['locations']['additional_locations'] ) ) {
$additional_locations = wc_clean( wp_unslash( $post['locations']['additional_locations'] ) );
$this->update_global_additional_locations( $additional_locations );
}
return $data;
}
/**
* Make meta data of SC CBL, protected
*
* @param bool $protected Is protected.
* @param string $meta_key The meta key.
* @param string $meta_type The meta type.
* @return bool $protected
*/
public function make_action_meta_protected( $protected, $meta_key, $meta_type ) {
$sc_meta = array(
'sa_cbl_locations_lookup_in' => '',
'sa_cbl_billing_locations' => '',
'sa_cbl_shipping_locations' => '',
);
if ( in_array( $meta_key, $sc_meta, true ) ) {
return true;
}
return $protected;
}
/**
* Function to copy CBL meta in newly generated coupon
*
* @param array $args The arguments.
*/
public function copy_coupon_action_meta( $args = array() ) {
$new_coupon_id = ( ! empty( $args['new_coupon_id'] ) ) ? absint( $args['new_coupon_id'] ) : 0;
$coupon = ( ! empty( $args['ref_coupon'] ) ) ? $args['ref_coupon'] : false;
if ( empty( $new_coupon_id ) || empty( $coupon ) ) {
return;
}
if ( $this->is_wc_gte_30() ) {
$locations_lookup_in = $coupon->get_meta( 'sa_cbl_locations_lookup_in' );
$billing_locations = $coupon->get_meta( 'sa_cbl_billing_locations' );
$shipping_locations = $coupon->get_meta( 'sa_cbl_shipping_locations' );
} else {
$old_coupon_id = ( ! empty( $coupon->id ) ) ? $coupon->id : 0;
$locations_lookup_in = get_post_meta( $old_coupon_id, 'sa_cbl_locations_lookup_in', true );
$billing_locations = get_post_meta( $old_coupon_id, 'sa_cbl_billing_locations', true );
$shipping_locations = get_post_meta( $old_coupon_id, 'sa_cbl_shipping_locations', true );
}
$new_coupon = new WC_Coupon( $new_coupon_id );
if ( $this->is_callable( $new_coupon, 'update_meta_data' ) && $this->is_callable( $new_coupon, 'save' ) ) {
$new_coupon->update_meta_data( 'sa_cbl_locations_lookup_in', $locations_lookup_in );
$new_coupon->update_meta_data( 'sa_cbl_billing_locations', $billing_locations );
$new_coupon->update_meta_data( 'sa_cbl_shipping_locations', $shipping_locations );
$new_coupon->save();
} else {
update_post_meta( $new_coupon_id, 'sa_cbl_locations_lookup_in', $locations_lookup_in );
update_post_meta( $new_coupon_id, 'sa_cbl_billing_locations', $billing_locations );
update_post_meta( $new_coupon_id, 'sa_cbl_shipping_locations', $shipping_locations );
}
if ( ! empty( $billing_locations['additional_locations'] ) ) {
$this->update_global_additional_locations( $billing_locations['additional_locations'] );
}
if ( ! empty( $shipping_locations['additional_locations'] ) ) {
$this->update_global_additional_locations( $shipping_locations['additional_locations'] );
}
}
/**
* Process coupon meta value for import
*
* @param mixed $meta_value The meta value.
* @param array $args Additional Arguments.
* @return mixed $meta_value
*/
public function process_coupon_meta_value_for_import( $meta_value = null, $args = array() ) {
if ( ! empty( $args['meta_key'] ) && in_array( $args['meta_key'], array( 'sa_cbl_locations_lookup_in', 'sa_cbl_billing_locations', 'sa_cbl_shipping_locations' ), true ) ) {
switch ( $args['meta_key'] ) {
case 'sa_cbl_locations_lookup_in':
$meta_value = ( ! empty( $args['postmeta']['sa_cbl_locations_lookup_in'] ) ) ? array( 'address' => wc_clean( wp_unslash( $args['postmeta']['sa_cbl_locations_lookup_in'] ) ) ) : array();
break;
case 'sa_cbl_billing_locations':
$meta_value = ( ! empty( $args['postmeta']['sa_cbl_billing_locations'] ) ) ? array( 'additional_locations' => explode( '|', wc_clean( wp_unslash( $args['postmeta']['sa_cbl_billing_locations'] ) ) ) ) : array();
break;
case 'sa_cbl_shipping_locations':
$meta_value = ( ! empty( $args['postmeta']['sa_cbl_shipping_locations'] ) ) ? array( 'additional_locations' => explode( '|', wc_clean( wp_unslash( $args['postmeta']['sa_cbl_shipping_locations'] ) ) ) ) : array();
break;
}
if ( in_array( $args['meta_key'], array( 'sa_cbl_billing_locations', 'sa_cbl_shipping_locations' ), true ) && ! empty( $meta_value['additional_locations'] ) ) {
$this->update_global_additional_locations( $meta_value['additional_locations'] );
}
}
return $meta_value;
}
/**
* Update global additional locations
*
* @param array $additional_locations The locations.
*/
public function update_global_additional_locations( $additional_locations = array() ) {
if ( empty( $additional_locations ) ) {
return;
}
$additional_locations = array_map( 'strtolower', $additional_locations );
$wc_countries = array_map( 'strtolower', WC()->countries->countries );
foreach ( $wc_countries as $index => $country ) {
$encoding = $this->mb_detect_encoding( $country, 'UTF-8, ISO-8859-1', true );
$wc_countries[ $index ] = ( false !== $encoding ) ? html_entity_decode( $country, ENT_COMPAT, $encoding ) : $country;
}
if ( count( $additional_locations ) > 0 ) {
$custom_locations = array();
// Loop through all location entered in Billing location of coupons.
// and collect those location which is not available in WooCommerce countries.
foreach ( $additional_locations as $location ) {
if ( ! in_array( $location, $wc_countries, true ) ) {
$custom_locations[] = $location;
}
}
$global_additional_locations = ( ! empty( $this->global_additional_locations ) ) ? $this->global_additional_locations : get_option( 'sa_cbl_additional_locations', array() );
// Add new location with already saved locations.
if ( false !== $global_additional_locations && ! empty( $global_additional_locations ) ) {
$global_additional_locations = array_merge( $global_additional_locations, $custom_locations );
} else {
$global_additional_locations = $custom_locations;
}
// Discard duplicate values, arrange alphabetically & save.
$global_additional_locations = array_unique( $global_additional_locations );
sort( $global_additional_locations );
update_option( 'sa_cbl_additional_locations', $global_additional_locations, 'no' );
}
}
}
}
WC_SC_Coupons_By_Location::get_instance();

View File

@@ -0,0 +1,420 @@
<?php
/**
* Class to handle feature Coupons By Payment Method
*
* @author StoreApps
* @category Admin
* @package wocommerce-smart-coupons/includes
* @version 1.7.1
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
if ( ! class_exists( 'WC_SC_Coupons_By_Payment_Method' ) ) {
/**
* Class WC_SC_Coupons_By_Payment_Method
*/
class WC_SC_Coupons_By_Payment_Method {
/**
* Variable to hold instance of this class
*
* @var $instance
*/
private static $instance = null;
/**
* Constructor
*/
private function __construct() {
add_action( 'woocommerce_coupon_options_usage_restriction', array( $this, 'usage_restriction' ), 10, 2 );
add_action( 'woocommerce_coupon_options_save', array( $this, 'process_meta' ), 10, 2 );
add_filter( 'woocommerce_coupon_is_valid', array( $this, 'validate' ), 11, 3 );
add_filter( 'wc_smart_coupons_export_headers', array( $this, 'export_headers' ) );
add_filter( 'wc_sc_export_coupon_meta', array( $this, 'export_coupon_meta_data' ), 10, 2 );
add_filter( 'smart_coupons_parser_postmeta_defaults', array( $this, 'postmeta_defaults' ) );
add_filter( 'sc_generate_coupon_meta', array( $this, 'generate_coupon_meta' ), 10, 2 );
add_filter( 'wc_sc_process_coupon_meta_value_for_import', array( $this, 'process_coupon_meta_value_for_import' ), 10, 2 );
add_filter( 'is_protected_meta', array( $this, 'make_action_meta_protected' ), 10, 3 );
add_action( 'wc_sc_new_coupon_generated', array( $this, 'copy_coupon_payment_method_meta' ) );
add_action( 'wp_footer', array( $this, 'styles_and_scripts' ) );
}
/**
* Get single instance of this class
*
* @return this class Singleton object of this class
*/
public static function get_instance() {
// Check if instance is already exists.
if ( is_null( self::$instance ) ) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Handle call to functions which is not available in this class
*
* @param string $function_name The function name.
* @param array $arguments Array of arguments passed while calling $function_name.
* @return result of function call
*/
public function __call( $function_name = '', $arguments = array() ) {
global $woocommerce_smart_coupon;
if ( ! is_callable( array( $woocommerce_smart_coupon, $function_name ) ) ) {
return;
}
if ( ! empty( $arguments ) ) {
return call_user_func_array( array( $woocommerce_smart_coupon, $function_name ), $arguments );
} else {
return call_user_func( array( $woocommerce_smart_coupon, $function_name ) );
}
}
/**
* Display field for coupon by payment method
*
* @param integer $coupon_id The coupon id.
* @param WC_Coupon $coupon The coupon object.
*/
public function usage_restriction( $coupon_id = 0, $coupon = null ) {
$payment_method_ids = array();
if ( ! empty( $coupon_id ) ) {
$payment_method_ids = $this->get_post_meta( $coupon_id, 'wc_sc_payment_method_ids', true );
if ( empty( $payment_method_ids ) || ! is_array( $payment_method_ids ) ) {
$payment_method_ids = array();
}
}
$available_payment_methods = WC()->payment_gateways->get_available_payment_gateways();
?>
<div class="options_group smart-coupons-field">
<p class="form-field">
<label for="wc_sc_payment_method_ids"><?php echo esc_html__( 'Payment methods', 'woocommerce-smart-coupons' ); ?></label>
<select id="wc_sc_payment_method_ids" name="wc_sc_payment_method_ids[]" style="width: 50%;" class="wc-enhanced-select" multiple="multiple" data-placeholder="<?php esc_attr_e( 'No payment methods', 'woocommerce-smart-coupons' ); ?>">
<?php
if ( is_array( $available_payment_methods ) && ! empty( $available_payment_methods ) ) {
foreach ( $available_payment_methods as $payment_method ) {
echo '<option value="' . esc_attr( $payment_method->id ) . '"' . esc_attr( selected( in_array( $payment_method->id, $payment_method_ids, true ), true, false ) ) . '>' . esc_html( $payment_method->get_title() ) . '</option>';
}
}
?>
</select>
<?php
$tooltip_text = esc_html__( 'Payment methods that must be selected during checkout for this coupon to be valid.', 'woocommerce-smart-coupons' );
echo wc_help_tip( $tooltip_text ); // phpcs:ignore
?>
</p>
</div>
<?php
}
/**
* Save coupon by payment method data in meta
*
* @param Integer $post_id The coupon post ID.
* @param WC_Coupon $coupon The coupon object.
*/
public function process_meta( $post_id = 0, $coupon = null ) {
if ( empty( $post_id ) ) {
return;
}
$coupon = new WC_Coupon( $coupon );
$payment_method_ids = ( isset( $_POST['wc_sc_payment_method_ids'] ) ) ? wc_clean( wp_unslash( $_POST['wc_sc_payment_method_ids'] ) ) : array(); // phpcs:ignore
if ( $this->is_callable( $coupon, 'update_meta_data' ) && $this->is_callable( $coupon, 'save' ) ) {
$coupon->update_meta_data( 'wc_sc_payment_method_ids', $payment_method_ids );
$coupon->save();
} else {
$this->update_post_meta( $post_id, 'wc_sc_payment_method_ids', $payment_method_ids );
}
}
/**
* Validate the coupon based on payment method
*
* @param boolean $valid Is valid or not.
* @param WC_Coupon $coupon The coupon object.
* @param WC_Discounts $discounts The discount object.
*
* @throws Exception If the coupon is invalid.
* @return boolean Is valid or not
*/
public function validate( $valid = false, $coupon = object, $discounts = object ) {
// If coupon is already invalid, no need for further checks.
if ( false === $valid ) {
return $valid;
}
if ( ! $coupon instanceof WC_Coupon ) {
return $valid;
}
if ( ! $discounts instanceof WC_Discounts ) {
return $valid;
}
if ( $this->is_wc_gte_30() ) {
$coupon_id = ( ! empty( $coupon ) && is_callable( array( $coupon, 'get_id' ) ) ) ? $coupon->get_id() : 0;
$coupon_code = ( ! empty( $coupon ) && is_callable( array( $coupon, 'get_code' ) ) ) ? $coupon->get_code() : '';
} else {
$coupon_id = ( ! empty( $coupon->id ) ) ? $coupon->id : 0;
$coupon_code = ( ! empty( $coupon->code ) ) ? $coupon->code : '';
}
if ( empty( $coupon_id ) ) {
return $valid;
}
$payment_method_ids = $this->get_post_meta( $coupon_id, 'wc_sc_payment_method_ids', true );
$cart_or_order_object = is_callable( array( $discounts, 'get_object' ) ) ? $discounts->get_object() : null;
$is_wc_session = is_a( $cart_or_order_object, 'WC_Cart' ) && function_exists( 'WC' ) && isset( WC()->session ) && is_object( WC()->session );
$needs_payment = ( $this->is_callable( $cart_or_order_object, 'needs_payment' ) ) ? $cart_or_order_object->needs_payment() : true;
$posted_data = array();
$post_data = isset( $_POST['post_data'] ) && ! empty( $_POST['post_data'] ) ? wc_clean( wp_unslash( $_POST['post_data'] ) ) : ''; // phpcs:ignore
wp_parse_str( $post_data, $posted_data );
if ( true === $needs_payment && is_array( $payment_method_ids ) && ! empty( $payment_method_ids ) ) {
$payment_titles = $this->get_payment_method_titles_by_ids( $payment_method_ids );
$chosen_payment_method = '';
if ( is_a( $cart_or_order_object, 'WC_Order' ) ) {
$chosen_payment_method = is_callable( array( $cart_or_order_object, 'get_payment_method' ) ) ? $cart_or_order_object->get_payment_method() : '';
} elseif ( ! empty( $posted_data ) && isset( $posted_data['payment_method'] ) ) {
$chosen_payment_method = $posted_data['payment_method'];
} elseif ( true === $is_wc_session ) {
$chosen_payment_method = ( WC()->session->__isset( 'chosen_payment_method' ) ) ? WC()->session->get( 'chosen_payment_method' ) : '';
}
if ( ! in_array( $chosen_payment_method, $payment_method_ids, true ) ) {
if ( true === $is_wc_session && is_callable( array( WC()->session, 'set' ) ) ) {
WC()->session->set( 'wc_sc_reload_payment_method', 'yes' );
}
$applied_coupons = ( WC()->cart instanceof WC_Cart && is_callable( array( WC()->cart, 'get_applied_coupons' ) ) ) ? WC()->cart->get_applied_coupons() : array();
if ( ! empty( $applied_coupons ) && in_array( $coupon_code, $applied_coupons, true ) ) {
WC()->cart->remove_coupon( $coupon_code );
/* translators: 1. The coupon code 2. The text 'payment method/s' 3. List of payment method names 4. Link to the checkout page */
wc_add_notice( sprintf( __( 'Coupon code %1$s has been removed. It is valid only for %2$s: %3$s. You can change the payment method from the %4$s page.', 'woocommerce-smart-coupons' ), '<code>' . $coupon_code . '</code>', _n( 'payment method', 'payment methods', count( $payment_titles ), 'woocommerce-smart-coupons' ), '<strong>"' . implode( '", "', $payment_titles ) . '"</strong>', '<a href="' . esc_url( wc_get_checkout_url() ) . '"><strong>' . __( 'Checkout', 'woocommerce-smart-coupons' ) . '</strong></a>' ), 'error' );
}
/* translators: 1. The coupon code 2. The text 'payment method/s' 3. List of payment method names 4. Link to the checkout page */
throw new Exception( sprintf( __( 'Coupon code %1$s is valid only for %2$s: %3$s. You can change payment method from the %4$s page.', 'woocommerce-smart-coupons' ), '<code>' . $coupon_code . '</code>', _n( 'payment method', 'payment methods', count( $payment_titles ), 'woocommerce-smart-coupons' ), '<strong>"' . implode( '", "', $payment_titles ) . '"</strong>', '<a href="' . esc_url( wc_get_checkout_url() ) . '"><strong>' . __( 'Checkout', 'woocommerce-smart-coupons' ) . '</strong></a>' ) );
}
if ( true === $is_wc_session && is_callable( array( WC()->session, 'set' ) ) ) {
WC()->session->set( 'wc_sc_reload_payment_method', 'no' );
}
}
return $valid;
}
/**
* Add meta in export headers
*
* @param array $headers Existing headers.
* @return array
*/
public function export_headers( $headers = array() ) {
$headers['wc_sc_payment_method_ids'] = __( 'Payment methods', 'woocommerce-smart-coupons' );
return $headers;
}
/**
* Function to handle coupon meta data during export of existing coupons
*
* @param mixed $meta_value The meta value.
* @param array $args Additional arguments.
* @return string Processed meta value
*/
public function export_coupon_meta_data( $meta_value = '', $args = array() ) {
if ( ! empty( $args['meta_key'] ) && 'wc_sc_payment_method_ids' === $args['meta_key'] ) {
if ( isset( $args['meta_value'] ) && ! empty( $args['meta_value'] ) ) {
$payment_method_ids = maybe_unserialize( stripslashes( $args['meta_value'] ) );
if ( is_array( $payment_method_ids ) && ! empty( $payment_method_ids ) ) {
$payment_method_titles = $this->get_payment_method_titles_by_ids( $payment_method_ids );
if ( is_array( $payment_method_titles ) && ! empty( $payment_method_titles ) ) {
$meta_value = implode( '|', wc_clean( wp_unslash( $payment_method_titles ) ) ); // Replace payment method ids with their respective method titles.
}
}
}
}
return $meta_value;
}
/**
* Post meta defaults for payment method ids meta
*
* @param array $defaults Existing postmeta defaults.
* @return array $defaults Modified postmeta defaults
*/
public function postmeta_defaults( $defaults = array() ) {
$defaults['wc_sc_payment_method_ids'] = '';
return $defaults;
}
/**
* Add payment method's meta with value in coupon meta
*
* @param array $data The row data.
* @param array $post The POST values.
* @return array $data Modified row data
*/
public function generate_coupon_meta( $data = array(), $post = array() ) {
$payment_method_titles = '';
if ( ! empty( $post['wc_sc_payment_method_ids'] ) && is_array( $post['wc_sc_payment_method_ids'] ) ) {
$payment_method_titles = $this->get_payment_method_titles_by_ids( $post['wc_sc_payment_method_ids'] );
if ( is_array( $payment_method_titles ) && ! empty( $payment_method_titles ) ) {
$payment_method_titles = implode( '|', wc_clean( wp_unslash( $payment_method_titles ) ) );
}
}
$data['wc_sc_payment_method_ids'] = $payment_method_titles; // Replace payment method ids with their respective method titles.
return $data;
}
/**
* Function to get payment method titles for given payment method ids
*
* @param array $payment_method_ids ids of payment methods.
* @return array $payment_method_titles titles of payment methods
*/
public function get_payment_method_titles_by_ids( $payment_method_ids = array() ) {
$payment_method_titles = array();
if ( is_array( $payment_method_ids ) && ! empty( $payment_method_ids ) ) {
$available_payment_methods = WC()->payment_gateways->get_available_payment_gateways();
foreach ( $payment_method_ids as $index => $payment_method_id ) {
$payment_method = ( isset( $available_payment_methods[ $payment_method_id ] ) && ! empty( $available_payment_methods[ $payment_method_id ] ) ) ? $available_payment_methods[ $payment_method_id ] : '';
if ( ! empty( $payment_method ) && is_a( $payment_method, 'WC_Payment_Gateway' ) ) {
$payment_method_title = is_callable( array( $payment_method, 'get_title' ) ) ? $payment_method->get_title() : '';
if ( ! empty( $payment_method_title ) ) {
$payment_method_titles[ $index ] = $payment_method_title; // Replace payment method id with it's repective title.
} else {
$payment_method_titles[ $index ] = $payment_method->id; // In case of empty payment method title replace it with method id.
}
}
}
}
return $payment_method_titles;
}
/**
* Process coupon meta value for import
*
* @param mixed $meta_value The meta value.
* @param array $args Additional Arguments.
* @return mixed $meta_value
*/
public function process_coupon_meta_value_for_import( $meta_value = null, $args = array() ) {
if ( ! empty( $args['meta_key'] ) && 'wc_sc_payment_method_ids' === $args['meta_key'] ) {
$meta_value = ( ! empty( $args['postmeta']['wc_sc_payment_method_ids'] ) ) ? explode( '|', wc_clean( wp_unslash( $args['postmeta']['wc_sc_payment_method_ids'] ) ) ) : array();
if ( is_array( $meta_value ) && ! empty( $meta_value ) ) {
$available_payment_methods = WC()->payment_gateways->get_available_payment_gateways();
if ( is_array( $available_payment_methods ) && ! empty( $available_payment_methods ) ) {
foreach ( $meta_value as $index => $payment_method_title ) {
foreach ( $available_payment_methods as $payment_method ) {
$method_title = is_callable( array( $payment_method, 'get_title' ) ) ? $payment_method->get_title() : '';
if ( $method_title === $payment_method_title && ! empty( $payment_method->id ) ) {
$meta_value[ $index ] = $payment_method->id; // Replace payment method title with it's respective id.
}
}
}
}
}
}
return $meta_value;
}
/**
* Function to copy payment method restriction meta in newly generated coupon
*
* @param array $args The arguments.
*/
public function copy_coupon_payment_method_meta( $args = array() ) {
// Copy meta data to new coupon.
$this->copy_coupon_meta_data(
$args,
array( 'wc_sc_payment_method_ids' )
);
}
/**
* Make meta data of payment method ids protected
*
* @param bool $protected Is protected.
* @param string $meta_key The meta key.
* @param string $meta_type The meta type.
* @return bool $protected
*/
public function make_action_meta_protected( $protected = false, $meta_key = '', $meta_type = '' ) {
if ( 'wc_sc_payment_method_ids' === $meta_key ) {
return true;
}
return $protected;
}
/**
* Function to add styles & scripts
*/
public function styles_and_scripts() {
if ( is_woocommerce() || is_cart() || is_checkout() || is_product_category() || is_product_tag() || is_account_page() ) {
$js = "
try {
let checkoutForm = document.querySelector('form.checkout');
if( checkoutForm ){
checkoutForm.addEventListener('change', function(event) {
if (event.target && event.target.name === 'payment_method') {
setTimeout(function() {
document.body.dispatchEvent(new Event('update_checkout'));
}, 1000);
}
});
}
} catch( error ) {
console.error( '" . __( 'An error occurred:', 'woocommerce-smart-coupons' ) . "', error);
}";
wc_enqueue_js( $js );
}
}
}
}
WC_SC_Coupons_By_Payment_Method::get_instance();

View File

@@ -0,0 +1,644 @@
<?php
/**
* Class to handle feature Coupons By Product Attribute
*
* @author StoreApps
* @category Admin
* @package wocommerce-smart-coupons/includes
* @version 1.8.0
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
if ( ! class_exists( 'WC_SC_Coupons_By_Product_Attribute' ) ) {
/**
* Class WC_SC_Coupons_By_Product_Attribute
*/
class WC_SC_Coupons_By_Product_Attribute {
/**
* Variable to hold instance of this class
*
* @var $instance
*/
private static $instance = null;
/**
* Constructor
*/
private function __construct() {
add_action( 'woocommerce_coupon_options_usage_restriction', array( $this, 'usage_restriction' ), 10, 2 );
add_action( 'woocommerce_coupon_options_save', array( $this, 'process_meta' ), 10, 2 );
add_filter( 'woocommerce_coupon_is_valid_for_product', array( $this, 'validate' ), 11, 4 );
add_filter( 'woocommerce_coupon_is_valid', array( $this, 'handle_non_product_type_coupons' ), 11, 3 );
add_filter( 'wc_smart_coupons_export_headers', array( $this, 'export_headers' ) );
add_filter( 'smart_coupons_parser_postmeta_defaults', array( $this, 'postmeta_defaults' ) );
add_filter( 'is_protected_meta', array( $this, 'make_action_meta_protected' ), 10, 3 );
add_filter( 'sc_generate_coupon_meta', array( $this, 'generate_coupon_attribute_meta' ), 10, 2 );
add_action( 'wc_sc_new_coupon_generated', array( $this, 'copy_coupon_attributes_meta' ) );
}
/**
* Get single instance of this class
*
* @return this class Singleton object of this class
*/
public static function get_instance() {
// Check if instance is already exists.
if ( is_null( self::$instance ) ) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Handle call to functions which is not available in this class
*
* @param string $function_name The function name.
* @param array $arguments Array of arguments passed while calling $function_name.
* @return result of function call
*/
public function __call( $function_name = '', $arguments = array() ) {
global $woocommerce_smart_coupon;
if ( ! is_callable( array( $woocommerce_smart_coupon, $function_name ) ) ) {
return;
}
if ( ! empty( $arguments ) ) {
return call_user_func_array( array( $woocommerce_smart_coupon, $function_name ), $arguments );
} else {
return call_user_func( array( $woocommerce_smart_coupon, $function_name ) );
}
}
/**
* Display field for coupon by product attribute
*
* @param integer $coupon_id The coupon id.
* @param WC_Coupon $coupon The coupon object.
*/
public function usage_restriction( $coupon_id = 0, $coupon = null ) {
global $wp_version;
$product_attribute_ids = array();
$exclude_product_attribute_ids = array();
$coupon_types = wc_get_coupon_types();
$product_coupon_types = wc_get_product_coupon_types();
$non_product_coupon_types = array();
foreach ( $coupon_types as $coupon_type => $coupon_label ) {
if ( ! in_array( $coupon_type, $product_coupon_types, true ) ) {
$non_product_coupon_types[] = $coupon_label;
}
}
if ( ! empty( $non_product_coupon_types ) ) {
$non_product_coupon_types_label = '"' . implode( ', ', $non_product_coupon_types ) . '"';
} else {
$non_product_coupon_types_label = '';
}
if ( ! empty( $coupon_id ) ) {
$coupon = ( ! empty( $coupon_id ) ) ? new WC_Coupon( $coupon_id ) : null;
$is_callable_coupon_get_meta = $this->is_callable( $coupon, 'get_meta' );
$product_attribute_ids = ( true === $is_callable_coupon_get_meta ) ? $coupon->get_meta( 'wc_sc_product_attribute_ids' ) : get_post_meta( $coupon_id, 'wc_sc_product_attribute_ids', true );
if ( ! empty( $product_attribute_ids ) ) {
$product_attribute_ids = explode( '|', $product_attribute_ids );
} else {
$product_attribute_ids = array();
}
$exclude_product_attribute_ids = ( true === $is_callable_coupon_get_meta ) ? $coupon->get_meta( 'wc_sc_exclude_product_attribute_ids' ) : get_post_meta( $coupon_id, 'wc_sc_exclude_product_attribute_ids', true );
if ( ! empty( $exclude_product_attribute_ids ) ) {
$exclude_product_attribute_ids = explode( '|', $exclude_product_attribute_ids );
} else {
$exclude_product_attribute_ids = array();
}
}
$attribute_taxonomies = wc_get_attribute_taxonomies();
$attribute_options = array();
$attribute_taxonomies_label = array();
if ( ! empty( $attribute_taxonomies ) && is_array( $attribute_taxonomies ) ) {
$attribute_taxonomies_name = array();
foreach ( $attribute_taxonomies as $attribute_taxonomy ) {
$attribute_name = isset( $attribute_taxonomy->attribute_name ) ? $attribute_taxonomy->attribute_name : '';
$attribute_label = isset( $attribute_taxonomy->attribute_label ) ? $attribute_taxonomy->attribute_label : '';
if ( ! empty( $attribute_name ) && ! empty( $attribute_label ) ) {
$attribute_taxonomy_name = wc_attribute_taxonomy_name( $attribute_name );
$attribute_taxonomies_name[] = $attribute_taxonomy_name;
$attribute_taxonomies_label[ $attribute_taxonomy_name ] = $attribute_label;
}
}
if ( ! empty( $attribute_taxonomies_name ) ) {
$args = array(
'orderby' => 'name',
'hide_empty' => 0,
);
$args = apply_filters( 'woocommerce_product_attribute_terms', $args );
if ( version_compare( $wp_version, '4.5.0', '>=' ) ) {
$attribute_taxonomies_terms = get_terms(
array_merge(
array(
'taxonomy' => $attribute_taxonomies_name,
),
$args
)
);
} else {
$attribute_taxonomies_terms = get_terms( $attribute_taxonomies_name, $args );
}
if ( ! empty( $attribute_taxonomies_terms ) && is_array( $attribute_taxonomies_terms ) ) {
foreach ( $attribute_taxonomies_terms as $attribute_taxonomy_term ) {
$attribute_taxonomy = $attribute_taxonomy_term->taxonomy;
$attribute_taxonomy_label = isset( $attribute_taxonomies_label[ $attribute_taxonomy ] ) ? $attribute_taxonomies_label[ $attribute_taxonomy ] : '';
if ( empty( $attribute_taxonomy_label ) ) {
continue;
}
$attribute_term_id = $attribute_taxonomy_term->term_id;
$attribute_term_name = $attribute_taxonomy_term->name;
$attribute_title = __( 'Attribute=', 'woocommerce-smart-coupons' ) . $attribute_taxonomy_label . ':' . __( 'Value=', 'woocommerce-smart-coupons' ) . $attribute_term_name;
$attribute_label = $attribute_taxonomy_label . ': ' . $attribute_term_name;
$attribute_options[ $attribute_term_id ] = array(
'title' => $attribute_title,
'label' => $attribute_label,
);
}
}
}
}
?>
<div class="options_group smart-coupons-field">
<p class="form-field">
<label for="wc_sc_product_attribute_ids"><?php echo esc_html__( 'Product attributes', 'woocommerce-smart-coupons' ); ?></label>
<select id="wc_sc_product_attribute_ids" name="wc_sc_product_attribute_ids[]" style="width: 50%;" class="wc-enhanced-select" multiple="multiple" data-placeholder="<?php esc_attr_e( 'No product attributes', 'woocommerce-smart-coupons' ); ?>">
<?php
if ( ! empty( $attribute_options ) ) {
foreach ( $attribute_options as $attribute_id => $attribute_data ) {
echo '<option title="' . esc_attr( $attribute_data['title'] ) . '" value="' . esc_attr( $attribute_id ) . '"' . esc_attr( selected( in_array( (string) $attribute_id, $product_attribute_ids, true ), true, false ) ) . '>' . esc_html( $attribute_data['label'] ) . '</option>';
}
}
?>
</select>
<?php
/* translators: Non product type coupon labels */
$tooltip_text = sprintf( esc_html__( 'Product attributes that the coupon will be applied to, or that need to be in the cart in order for the %s to be applied.', 'woocommerce-smart-coupons' ), $non_product_coupon_types_label );
echo wc_help_tip( $tooltip_text ); // phpcs:ignore
?>
</p>
</div>
<div class="options_group smart-coupons-field">
<p class="form-field">
<label for="wc_sc_exclude_product_attribute_ids"><?php echo esc_html__( 'Exclude attributes', 'woocommerce-smart-coupons' ); ?></label>
<select id="wc_sc_exclude_product_attribute_ids" name="wc_sc_exclude_product_attribute_ids[]" style="width: 50%;" class="wc-enhanced-select" multiple="multiple" data-placeholder="<?php esc_attr_e( 'No product attributes', 'woocommerce-smart-coupons' ); ?>">
<?php
if ( ! empty( $attribute_options ) ) {
foreach ( $attribute_options as $attribute_id => $attribute_data ) {
echo '<option title="' . esc_attr( $attribute_data['title'] ) . '" value="' . esc_attr( $attribute_id ) . '"' . esc_attr( selected( in_array( (string) $attribute_id, $exclude_product_attribute_ids, true ), true, false ) ) . '>' . esc_html( $attribute_data['label'] ) . '</option>';
}
}
?>
</select>
<?php
/* translators: Non product type coupon labels */
$tooltip_text = sprintf( esc_html__( 'Product attributes that the coupon will not be applied to, or that cannot be in the cart in order for the %s to be applied.', 'woocommerce-smart-coupons' ), $non_product_coupon_types_label );
echo wc_help_tip( $tooltip_text ); // phpcs:ignore
?>
</p>
</div>
<?php
}
/**
* Save coupon by product attribute data in meta
*
* @param Integer $post_id The coupon post ID.
* @param WC_Coupon $coupon The coupon object.
*/
public function process_meta( $post_id = 0, $coupon = null ) {
if ( empty( $post_id ) ) {
return;
}
$coupon = new WC_Coupon( $coupon );
$is_callable_coupon_update_meta = $this->is_callable( $coupon, 'update_meta_data' );
$product_attribute_ids = ( isset( $_POST['wc_sc_product_attribute_ids'] ) ) ? wc_clean( wp_unslash( $_POST['wc_sc_product_attribute_ids'] ) ) : array(); // phpcs:ignore
$product_attribute_ids = implode( '|', $product_attribute_ids ); // Store attribute ids as delimited data instead of serialized data.
if ( true === $is_callable_coupon_update_meta ) {
$coupon->update_meta_data( 'wc_sc_product_attribute_ids', $product_attribute_ids );
} else {
update_post_meta( $post_id, 'wc_sc_product_attribute_ids', $product_attribute_ids );
}
$exclude_product_attribute_ids = ( isset( $_POST['wc_sc_exclude_product_attribute_ids'] ) ) ? wc_clean( wp_unslash( $_POST['wc_sc_exclude_product_attribute_ids'] ) ) : array(); // phpcs:ignore
$exclude_product_attribute_ids = implode( '|', $exclude_product_attribute_ids ); // Store attribute ids as delimited data instead of serialized data.
if ( true === $is_callable_coupon_update_meta ) {
$coupon->update_meta_data( 'wc_sc_exclude_product_attribute_ids', $exclude_product_attribute_ids );
} else {
update_post_meta( $post_id, 'wc_sc_exclude_product_attribute_ids', $exclude_product_attribute_ids );
}
if ( $this->is_callable( $coupon, 'save' ) ) {
$coupon->save();
}
}
/**
* Function to validate coupons for against product attributes
*
* @param bool $valid Coupon validity.
* @param WC_Product|null $product Product object.
* @param WC_Coupon|null $coupon Coupon object.
* @param array|null $values Values.
* @return bool $valid
*/
public function validate( $valid = false, $product = null, $coupon = null, $values = null ) {
$backtrace = wp_list_pluck( debug_backtrace( DEBUG_BACKTRACE_IGNORE_ARGS ), 'function' ); // phpcs:ignore
// If coupon is already invalid, no need for further checks.
// Ignore this check if the discount type is a non-product-type discount.
if ( true !== $valid && ! in_array( 'handle_non_product_type_coupons', $backtrace, true ) ) {
return $valid;
}
if ( empty( $product ) || empty( $coupon ) ) {
return $valid;
}
if ( $this->is_wc_gte_30() ) {
$coupon_id = ( is_object( $coupon ) && is_callable( array( $coupon, 'get_id' ) ) ) ? $coupon->get_id() : 0;
} else {
$coupon_id = ( ! empty( $coupon->id ) ) ? $coupon->id : 0;
}
if ( ! empty( $coupon_id ) ) {
$coupon = ( ! empty( $coupon_id ) ) ? new WC_Coupon( $coupon_id ) : null;
if ( $this->is_callable( $coupon, 'get_meta' ) ) {
$product_attribute_ids = $coupon->get_meta( 'wc_sc_product_attribute_ids' );
$exclude_product_attribute_ids = $coupon->get_meta( 'wc_sc_exclude_product_attribute_ids' );
} else {
$product_attribute_ids = get_post_meta( $coupon_id, 'wc_sc_product_attribute_ids', true );
$exclude_product_attribute_ids = get_post_meta( $coupon_id, 'wc_sc_exclude_product_attribute_ids', true );
}
if ( ! empty( $product_attribute_ids ) || ! empty( $exclude_product_attribute_ids ) ) {
$current_product_attribute_ids = $this->get_product_attributes( $product );
if ( ! empty( $product_attribute_ids ) ) {
$product_attribute_ids = explode( '|', $product_attribute_ids );
}
$product_attribute_found = true;
if ( ! empty( $product_attribute_ids ) && is_array( $product_attribute_ids ) ) {
$common_attribute_ids = array_intersect( $product_attribute_ids, $current_product_attribute_ids );
if ( count( $common_attribute_ids ) > 0 ) {
$product_attribute_found = true;
} else {
$product_attribute_found = false;
}
}
if ( ! empty( $exclude_product_attribute_ids ) ) {
$exclude_product_attribute_ids = explode( '|', $exclude_product_attribute_ids );
}
$exclude_attribute_found = false;
if ( ! empty( $exclude_product_attribute_ids ) && is_array( $exclude_product_attribute_ids ) ) {
$common_exclude_attribute_ids = array_intersect( $exclude_product_attribute_ids, $current_product_attribute_ids );
if ( count( $common_exclude_attribute_ids ) > 0 ) {
$exclude_attribute_found = true;
} else {
$exclude_attribute_found = false;
}
}
if ( false === $product_attribute_found && false === $exclude_attribute_found ) {
$product_parent_id = is_callable( array( $product, 'get_parent_id' ) ) ? $product->get_parent_id() : 0;
if ( ! empty( $product_parent_id ) ) {
$parent_product = ( function_exists( 'wc_get_product' ) ) ? wc_get_product( $product_parent_id ) : null;
if ( ! empty( $parent_product ) ) {
$parent_product_attribute_ids = $this->get_product_attributes( $parent_product );
if ( apply_filters( 'wc_sc_check_parent_attributes', true, $product ) && ! empty( $product_attribute_ids ) && is_array( $product_attribute_ids ) ) {
$parent_product_attribute_id = array_intersect( $product_attribute_ids, $parent_product_attribute_ids );
if ( count( $parent_product_attribute_id ) > 0 ) {
$product_attribute_found = true;
} else {
$product_attribute_found = false;
}
}
if ( apply_filters( 'wc_sc_check_parent_attributes', true, $product ) && ! empty( $exclude_product_attribute_ids ) && is_array( $exclude_product_attribute_ids ) ) {
$exclude_parent_product_attribute_id = array_intersect( $exclude_product_attribute_ids, $parent_product_attribute_ids );
if ( count( $exclude_parent_product_attribute_id ) > 0 ) {
$exclude_attribute_found = true;
} else {
$exclude_attribute_found = false;
}
}
}
}
}
$valid = ( $product_attribute_found && ! $exclude_attribute_found ) ? true : false;
}
}
return $valid;
}
/**
* Function to get product attributes of a given product.
*
* @param WC_Product $product Product.
* @return array $product_attributes_ids IDs of product attributes
*/
public function get_product_attributes( $product = null ) {
$product_attributes_ids = array();
if ( ! is_a( $product, 'WC_Product' ) ) {
// Check if product id has been passed.
if ( is_numeric( $product ) ) {
$product = wc_get_product( $product );
}
}
if ( ! is_a( $product, 'WC_Product' ) ) {
return $product_attribute_ids;
}
$product_attributes = $product->get_attributes();
if ( ! empty( $product_attributes ) ) {
if ( true === $product->is_type( 'variation' ) ) {
foreach ( $product_attributes as $variation_taxonomy => $variation_slug ) {
$variation_attribute = get_term_by( 'slug', $variation_slug, $variation_taxonomy );
if ( is_object( $variation_attribute ) ) {
$product_attributes_ids[] = $variation_attribute->term_id;
}
}
} else {
$product_id = ( is_object( $product ) && is_callable( array( $product, 'get_id' ) ) ) ? $product->get_id() : 0;
if ( ! empty( $product_id ) ) {
$is_variable = $product->is_type( 'variable' );
foreach ( $product_attributes as $attribute ) {
if ( true === $is_variable && isset( $attribute['is_variation'] ) && ! empty( $attribute['is_variation'] ) ) {
continue;
}
if ( isset( $attribute['is_taxonomy'] ) && ! empty( $attribute['is_taxonomy'] ) ) {
$attribute_taxonomy_name = $attribute['name'];
$product_term_ids = wc_get_product_terms( $product_id, $attribute_taxonomy_name, array( 'fields' => 'ids' ) );
if ( ! empty( $product_term_ids ) && is_array( $product_term_ids ) ) {
foreach ( $product_term_ids as $product_term_id ) {
$product_attributes_ids[] = $product_term_id;
}
}
}
}
}
}
}
return $product_attributes_ids;
}
/**
* Function to validate non product type coupons against product attribute restriction
* We need to remove coupon if it does not pass attribute validation even for single cart item in case of non product type coupons e.g fixed_cart, smart_coupon since these coupon type require all products in the cart to be valid
*
* @param boolean $valid Coupon validity.
* @param WC_Coupon $coupon Coupon object.
* @param WC_Discounts $discounts Discounts object.
* @throws Exception Validation exception.
* @return boolean $valid Coupon validity
*/
public function handle_non_product_type_coupons( $valid = true, $coupon = null, $discounts = null ) {
// If coupon is already invalid, no need for further checks.
if ( true !== $valid ) {
return $valid;
}
if ( ! is_a( $coupon, 'WC_Coupon' ) ) {
return $valid;
}
if ( $this->is_wc_gte_30() ) {
$coupon_id = ( is_object( $coupon ) && is_callable( array( $coupon, 'get_id' ) ) ) ? $coupon->get_id() : 0;
$discount_type = ( is_object( $coupon ) && is_callable( array( $coupon, 'get_discount_type' ) ) ) ? $coupon->get_discount_type() : '';
} else {
$coupon_id = ( ! empty( $coupon->id ) ) ? $coupon->id : 0;
$discount_type = ( ! empty( $coupon->discount_type ) ) ? $coupon->discount_type : '';
}
if ( ! empty( $coupon_id ) ) {
if ( $this->is_callable( $coupon, 'get_meta' ) ) {
$product_attribute_ids = $coupon->get_meta( 'wc_sc_product_attribute_ids' );
$exclude_product_attribute_ids = $coupon->get_meta( 'wc_sc_exclude_product_attribute_ids' );
} else {
$product_attribute_ids = get_post_meta( $coupon_id, 'wc_sc_product_attribute_ids', true );
$exclude_product_attribute_ids = get_post_meta( $coupon_id, 'wc_sc_exclude_product_attribute_ids', true );
}
// If product attributes are not set in coupon, stop further processing and return from here.
if ( empty( $product_attribute_ids ) && empty( $exclude_product_attribute_ids ) ) {
return $valid;
}
} else {
return $valid;
}
$product_coupon_types = wc_get_product_coupon_types();
// Proceed if it is non product type coupon.
if ( ! in_array( $discount_type, $product_coupon_types, true ) ) {
if ( class_exists( 'WC_Discounts' ) && isset( WC()->cart ) ) {
$wc_cart = WC()->cart;
$wc_discounts = new WC_Discounts( $wc_cart );
$items_to_validate = array();
if ( is_callable( array( $wc_discounts, 'get_items_to_validate' ) ) ) {
$items_to_validate = $wc_discounts->get_items_to_validate();
} elseif ( is_callable( array( $wc_discounts, 'get_items' ) ) ) {
$items_to_validate = $wc_discounts->get_items();
} elseif ( isset( $wc_discounts->items ) && is_array( $wc_discounts->items ) ) {
$items_to_validate = $wc_discounts->items;
}
if ( ! empty( $items_to_validate ) && is_array( $items_to_validate ) ) {
$valid_products = array();
$invalid_products = array();
foreach ( $items_to_validate as $item ) {
$cart_item = clone $item; // Clone the item so changes to wc_discounts item do not affect the originals.
$item_product = isset( $cart_item->product ) ? $cart_item->product : null;
$item_object = isset( $cart_item->object ) ? $cart_item->object : null;
if ( ! is_null( $item_product ) && ! is_null( $item_object ) ) {
if ( $coupon->is_valid_for_product( $item_product, $item_object ) ) {
$valid_products[] = $item_product;
} else {
$invalid_products[] = $item_product;
}
}
}
// If cart does not have any valid product then throw Exception.
if ( 0 === count( $valid_products ) ) {
$error_message = __( 'Sorry, this coupon is not applicable to selected products.', 'woocommerce-smart-coupons' );
$error_code = defined( 'E_WC_COUPON_NOT_APPLICABLE' ) ? E_WC_COUPON_NOT_APPLICABLE : 0;
throw new Exception( $error_message, $error_code );
} elseif ( count( $invalid_products ) > 0 && ! empty( $exclude_product_attribute_ids ) ) {
$exclude_product_attribute_ids = explode( '|', $exclude_product_attribute_ids );
$excluded_products = array();
foreach ( $invalid_products as $invalid_product ) {
$product_attributes = $this->get_product_attributes( $invalid_product );
if ( ! empty( $product_attributes ) && is_array( $product_attributes ) ) {
$common_exclude_attribute_ids = array_intersect( $exclude_product_attribute_ids, $product_attributes );
if ( count( $common_exclude_attribute_ids ) > 0 ) {
$excluded_products[] = $invalid_product->get_name();
} else {
$product_parent_id = is_callable( array( $invalid_product, 'get_parent_id' ) ) ? $invalid_product->get_parent_id() : 0;
if ( ! empty( $product_parent_id ) ) {
$parent_product = ( function_exists( 'wc_get_product' ) ) ? wc_get_product( $product_parent_id ) : '';
if ( ! empty( $parent_product ) ) {
$parent_product_attribute_ids = $this->get_product_attributes( $parent_product );
if ( apply_filters( 'wc_sc_check_parent_attributes', true, $invalid_product ) && ! empty( $exclude_product_attribute_ids ) && is_array( $exclude_product_attribute_ids ) ) {
$exclude_parent_product_attribute_id = array_intersect( $exclude_product_attribute_ids, $parent_product_attribute_ids );
if ( count( $exclude_parent_product_attribute_id ) > 0 ) {
$excluded_products[] = $invalid_product->get_name();
}
}
}
}
}
}
}
if ( count( $excluded_products ) > 0 ) {
// If cart contains any excluded product and it is being excluded from our excluded product attributes then throw Exception.
/* translators: 1. Singular/plural label for product(s) 2. Excluded product names */
$error_message = sprintf( __( 'Sorry, this coupon is not applicable to the %1$s: %2$s.', 'woocommerce-smart-coupons' ), _n( 'product', 'products', count( $excluded_products ), 'woocommerce-smart-coupons' ), implode( ', ', $excluded_products ) );
$error_code = defined( 'E_WC_COUPON_EXCLUDED_PRODUCTS' ) ? E_WC_COUPON_EXCLUDED_PRODUCTS : 0;
throw new Exception( $error_message, $error_code );
}
}
}
}
}
return $valid;
}
/**
* Add meta in export headers
*
* @param array $headers Existing headers.
* @return array
*/
public function export_headers( $headers = array() ) {
$headers['wc_sc_product_attribute_ids'] = __( 'Product Attributes', 'woocommerce-smart-coupons' );
$headers['wc_sc_exclude_product_attribute_ids'] = __( 'Exclude Attributes', 'woocommerce-smart-coupons' );
return $headers;
}
/**
* Post meta defaults for product attribute ids meta
*
* @param array $defaults Existing postmeta defaults.
* @return array $defaults Modified postmeta defaults
*/
public function postmeta_defaults( $defaults = array() ) {
$defaults['wc_sc_product_attribute_ids'] = '';
$defaults['wc_sc_exclude_product_attribute_ids'] = '';
return $defaults;
}
/**
* Make meta data of product attribute ids protected
*
* @param bool $protected Is protected.
* @param string $meta_key The meta key.
* @param string $meta_type The meta type.
* @return bool $protected
*/
public function make_action_meta_protected( $protected = false, $meta_key = '', $meta_type = '' ) {
$sc_product_attribute_keys = array(
'wc_sc_product_attribute_ids',
'wc_sc_exclude_product_attribute_ids',
);
if ( in_array( $meta_key, $sc_product_attribute_keys, true ) ) {
return true;
}
return $protected;
}
/**
* Add product's attribute in coupon meta
*
* @param array $data The row data.
* @param array $post The POST values.
* @return array Modified data
*/
public function generate_coupon_attribute_meta( $data = array(), $post = array() ) {
$product_attribute_ids = ( isset( $post['wc_sc_product_attribute_ids'] ) ) ? wc_clean( wp_unslash( $post['wc_sc_product_attribute_ids'] ) ) : array(); // phpcs:ignore
$data['wc_sc_product_attribute_ids'] = implode( '|', $product_attribute_ids ); // Store attribute ids as delimited data instead of serialized data.
$exclude_product_attribute_ids = ( isset( $post['wc_sc_exclude_product_attribute_ids'] ) ) ? wc_clean( wp_unslash( $post['wc_sc_exclude_product_attribute_ids'] ) ) : array(); // phpcs:ignore
$data['wc_sc_exclude_product_attribute_ids'] = implode( '|', $exclude_product_attribute_ids ); // Store attribute ids as delimited data instead of serialized data.
return $data;
}
/**
* Function to copy product's attribute meta in newly generated coupon
*
* @param array $args The arguments.
*/
public function copy_coupon_attributes_meta( $args = array() ) {
$new_coupon_id = ( ! empty( $args['new_coupon_id'] ) ) ? absint( $args['new_coupon_id'] ) : 0;
$coupon = ( ! empty( $args['ref_coupon'] ) ) ? $args['ref_coupon'] : false;
if ( empty( $new_coupon_id ) || empty( $coupon ) ) {
return;
}
if ( $this->is_wc_gte_30() ) {
$product_attribute_ids = $coupon->get_meta( 'wc_sc_product_attribute_ids' );
$exclude_product_attribute_ids = $coupon->get_meta( 'wc_sc_exclude_product_attribute_ids' );
} else {
$old_coupon_id = ( ! empty( $coupon->id ) ) ? $coupon->id : 0;
$product_attribute_ids = get_post_meta( $old_coupon_id, 'wc_sc_product_attribute_ids', true );
$exclude_product_attribute_ids = get_post_meta( $old_coupon_id, 'wc_sc_exclude_product_attribute_ids', true );
}
$new_coupon = new WC_Coupon( $new_coupon_id );
if ( $this->is_callable( $new_coupon, 'update_meta_data' ) && $this->is_callable( $new_coupon, 'save' ) ) {
$new_coupon->update_meta_data( 'wc_sc_product_attribute_ids', $product_attribute_ids );
$new_coupon->update_meta_data( 'wc_sc_exclude_product_attribute_ids', $exclude_product_attribute_ids );
$new_coupon->save();
} else {
update_post_meta( $new_coupon_id, 'wc_sc_product_attribute_ids', $product_attribute_ids );
update_post_meta( $new_coupon_id, 'wc_sc_exclude_product_attribute_ids', $exclude_product_attribute_ids );
}
}
}
}
WC_SC_Coupons_By_Product_Attribute::get_instance();

View File

@@ -0,0 +1,394 @@
<?php
/**
* Class to handle feature Coupons By Shipping Method
*
* @author StoreApps
* @category Admin
* @package wocommerce-smart-coupons/includes
* @version 1.8.0
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
if ( ! class_exists( 'WC_SC_Coupons_By_Shipping_Method' ) ) {
/**
* Class WC_SC_Coupons_By_Shipping_Method
*/
class WC_SC_Coupons_By_Shipping_Method {
/**
* Variable to hold instance of this class
*
* @var $instance
*/
private static $instance = null;
/**
* Constructor
*/
private function __construct() {
add_action( 'woocommerce_coupon_options_usage_restriction', array( $this, 'usage_restriction' ), 10, 2 );
add_action( 'woocommerce_coupon_options_save', array( $this, 'process_meta' ), 10, 2 );
add_filter( 'woocommerce_coupon_is_valid', array( $this, 'validate' ), 11, 3 );
add_filter( 'wc_smart_coupons_export_headers', array( $this, 'export_headers' ) );
add_filter( 'wc_sc_export_coupon_meta', array( $this, 'export_coupon_meta_data' ), 10, 2 );
add_filter( 'smart_coupons_parser_postmeta_defaults', array( $this, 'postmeta_defaults' ) );
add_filter( 'sc_generate_coupon_meta', array( $this, 'generate_coupon_meta' ), 10, 2 );
add_filter( 'wc_sc_process_coupon_meta_value_for_import', array( $this, 'process_coupon_meta_value_for_import' ), 10, 2 );
add_filter( 'is_protected_meta', array( $this, 'make_action_meta_protected' ), 10, 3 );
add_action( 'wc_sc_new_coupon_generated', array( $this, 'copy_coupon_shipping_method_meta' ) );
// Register the action for shipping change via Block API.
add_action( 'woocommerce_store_api_cart_select_shipping_rate', array( $this, 'wc_blocks_sc_shipping_change_auto_apply_coupon' ), 10, 3 );
}
/**
* Get single instance of this class
*
* @return this class Singleton object of this class
*/
public static function get_instance() {
// Check if instance is already exists.
if ( is_null( self::$instance ) ) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Handle call to functions which is not available in this class
*
* @param string $function_name The function name.
* @param array $arguments Array of arguments passed while calling $function_name.
* @return result of function call
*/
public function __call( $function_name = '', $arguments = array() ) {
global $woocommerce_smart_coupon;
if ( ! is_callable( array( $woocommerce_smart_coupon, $function_name ) ) ) {
return;
}
if ( ! empty( $arguments ) ) {
return call_user_func_array( array( $woocommerce_smart_coupon, $function_name ), $arguments );
} else {
return call_user_func( array( $woocommerce_smart_coupon, $function_name ) );
}
}
/**
* Display field for coupon by shipping method
*
* @param integer $coupon_id The coupon id.
* @param WC_Coupon $coupon The coupon object.
*/
public function usage_restriction( $coupon_id = 0, $coupon = null ) {
$shipping_method_ids = array();
if ( ! empty( $coupon_id ) ) {
$shipping_method_ids = ( $this->is_callable( $coupon, 'get_meta' ) ) ? $coupon->get_meta( 'wc_sc_shipping_method_ids' ) : $this->get_post_meta( $coupon_id, 'wc_sc_shipping_method_ids', true );
if ( empty( $shipping_method_ids ) || ! is_array( $shipping_method_ids ) ) {
$shipping_method_ids = array();
}
}
$available_shipping_methods = WC()->shipping->get_shipping_methods();
?>
<div class="options_group smart-coupons-field">
<p class="form-field">
<label for="wc_sc_shipping_method_ids"><?php echo esc_html__( 'Shipping methods', 'woocommerce-smart-coupons' ); ?></label>
<select id="wc_sc_shipping_method_ids" name="wc_sc_shipping_method_ids[]" style="width: 50%;" class="wc-enhanced-select" multiple="multiple" data-placeholder="<?php esc_attr_e( 'No shipping methods', 'woocommerce-smart-coupons' ); ?>">
<?php
if ( is_array( $available_shipping_methods ) && ! empty( $available_shipping_methods ) ) {
foreach ( $available_shipping_methods as $shipping_method ) {
echo '<option value="' . esc_attr( $shipping_method->id ) . '"' . esc_attr( selected( in_array( $shipping_method->id, $shipping_method_ids, true ), true, false ) ) . '>' . esc_html( $shipping_method->get_method_title() ) . '</option>';
}
}
?>
</select>
<?php
$tooltip_text = esc_html__( 'Shipping methods that must be selected during checkout for this coupon to be valid.', 'woocommerce-smart-coupons' );
echo wc_help_tip( $tooltip_text ); // phpcs:ignore
?>
</p>
</div>
<?php
}
/**
* Save coupon by shipping method data in meta
*
* @param Integer $post_id The coupon post ID.
* @param WC_Coupon $coupon The coupon object.
*/
public function process_meta( $post_id = 0, $coupon = null ) {
if ( empty( $post_id ) ) {
return;
}
$coupon = new WC_Coupon( $coupon );
$shipping_method_ids = ( isset( $_POST['wc_sc_shipping_method_ids'] ) ) ? wc_clean( wp_unslash( $_POST['wc_sc_shipping_method_ids'] ) ) : array(); // phpcs:ignore
if ( $this->is_callable( $coupon, 'update_meta_data' ) && $this->is_callable( $coupon, 'save' ) ) {
$coupon->update_meta_data( 'wc_sc_shipping_method_ids', $shipping_method_ids );
$coupon->save();
} else {
$this->update_post_meta( $post_id, 'wc_sc_shipping_method_ids', $shipping_method_ids );
}
}
/**
* Validate the coupon based on shipping method
*
* @param boolean $valid Is valid or not.
* @param WC_Coupon $coupon The coupon object.
* @param WC_Discounts $discounts The discount object.
*
* @throws Exception If the coupon is invalid.
* @return boolean Is valid or not
*/
public function validate( $valid = false, $coupon = object, $discounts = null ) {
// If coupon is already invalid, no need for further checks.
if ( false === $valid ) {
return $valid;
}
$coupon_id = ( $this->is_wc_gte_30() ) ? $coupon->get_id() : $coupon->id;
$shipping_method_ids = ( $this->is_callable( $coupon, 'get_meta' ) ) ? $coupon->get_meta( 'wc_sc_shipping_method_ids' ) : $this->get_post_meta( $coupon_id, 'wc_sc_shipping_method_ids', true );
if ( is_array( $shipping_method_ids ) && ! empty( $shipping_method_ids ) ) {
$chosen_shipping_method_data = WC()->session->__isset( 'chosen_shipping_methods' ) ? WC()->session->get( 'chosen_shipping_methods' ) : '';
$chosen_shipping_method_string = is_array( $chosen_shipping_method_data ) && ! empty( $chosen_shipping_method_data ) ? $chosen_shipping_method_data[0] : '';
if ( ! empty( $chosen_shipping_method_string ) ) {
$chosen_shipping_method_string = explode( ':', $chosen_shipping_method_string );
$chosen_shipping_method_id = $chosen_shipping_method_string[0];
if ( ! in_array( $chosen_shipping_method_id, $shipping_method_ids, true ) ) {
$wc_shipping_packages = ( is_callable( array( WC()->shipping, 'get_packages' ) ) ) ? WC()->shipping->get_packages() : null;
if ( empty( $wc_shipping_packages ) && is_callable( array( WC()->cart, 'calculate_shipping' ) ) ) {
WC()->cart->calculate_shipping();
}
$chosen_shipping_method_rate_id = is_array( $chosen_shipping_method_data ) && ! empty( $chosen_shipping_method_data ) ? $chosen_shipping_method_data[0] : '';
$shipping_method_id = '';
$available_shipping_packages = ( is_callable( array( WC()->shipping, 'get_packages' ) ) ) ? WC()->shipping->get_packages() : '';
if ( ! empty( $available_shipping_packages ) ) {
foreach ( $available_shipping_packages as $key => $package ) {
if ( ! empty( $shipping_method_id ) ) {
break;
}
// Loop through Shipping rates.
if ( isset( $package['rates'] ) && ! empty( $package['rates'] ) ) {
foreach ( $package['rates'] as $rate_id => $rate ) {
if ( $chosen_shipping_method_rate_id === $rate_id ) {
$shipping_method_id = ( is_callable( array( $rate, 'get_method_id' ) ) ) ? $rate->get_method_id() : '';
break;
}
}
}
}
if ( ! in_array( $shipping_method_id, $shipping_method_ids, true ) ) {
if ( ! apply_filters( 'wc_sc_coupon_validate_shipping_method', false, $chosen_shipping_method_id, $shipping_method_ids ) ) {
throw new Exception( __( 'This coupon is not valid for selected shipping method.', 'woocommerce-smart-coupons' ) );
}
}
} else {
if ( ! apply_filters( 'wc_sc_coupon_validate_shipping_method', false, $chosen_shipping_method_id, $shipping_method_ids ) ) {
throw new Exception( __( 'This coupon is not valid for selected shipping method.', 'woocommerce-smart-coupons' ) );
}
}
}
}
}
return $valid;
}
/**
* Add meta in export headers
*
* @param array $headers Existing headers.
* @return array
*/
public function export_headers( $headers = array() ) {
$headers['wc_sc_shipping_method_ids'] = __( 'Shipping methods', 'woocommerce-smart-coupons' );
return $headers;
}
/**
* Function to handle coupon meta data during export of existing coupons
*
* @param mixed $meta_value The meta value.
* @param array $args Additional arguments.
* @return string Processed meta value
*/
public function export_coupon_meta_data( $meta_value = '', $args = array() ) {
if ( ! empty( $args['meta_key'] ) && 'wc_sc_shipping_method_ids' === $args['meta_key'] ) {
if ( isset( $args['meta_value'] ) && ! empty( $args['meta_value'] ) ) {
$shipping_method_ids = maybe_unserialize( stripslashes( $args['meta_value'] ) );
if ( is_array( $shipping_method_ids ) && ! empty( $shipping_method_ids ) ) {
$shipping_method_titles = $this->get_shipping_method_titles_by_ids( $shipping_method_ids );
if ( is_array( $shipping_method_titles ) && ! empty( $shipping_method_titles ) ) {
$meta_value = implode( '|', wc_clean( wp_unslash( $shipping_method_titles ) ) ); // Replace shipping method ids with their respective method titles.
}
}
}
}
return $meta_value;
}
/**
* Post meta defaults for shipping method ids meta
*
* @param array $defaults Existing postmeta defaults.
* @return array $defaults Modified postmeta defaults
*/
public function postmeta_defaults( $defaults = array() ) {
$defaults['wc_sc_shipping_method_ids'] = '';
return $defaults;
}
/**
* Add shipping method's meta with value in coupon meta
*
* @param array $data The row data.
* @param array $post The POST values.
* @return array $data Modified row data
*/
public function generate_coupon_meta( $data = array(), $post = array() ) {
$shipping_method_titles = '';
if ( ! empty( $post['wc_sc_shipping_method_ids'] ) && is_array( $post['wc_sc_shipping_method_ids'] ) ) {
$shipping_method_titles = $this->get_shipping_method_titles_by_ids( $post['wc_sc_shipping_method_ids'] );
if ( is_array( $shipping_method_titles ) && ! empty( $shipping_method_titles ) ) {
$shipping_method_titles = implode( '|', wc_clean( wp_unslash( $shipping_method_titles ) ) );
}
}
$data['wc_sc_shipping_method_ids'] = $shipping_method_titles; // Replace shipping method ids with their respective method titles.
return $data;
}
/**
* Function to get shipping method titles for given shipping method ids
*
* @param array $shipping_method_ids ids of shipping methods.
* @return array $shipping_method_titles titles of shipping methods
*/
public function get_shipping_method_titles_by_ids( $shipping_method_ids = array() ) {
$shipping_method_titles = array();
if ( is_array( $shipping_method_ids ) && ! empty( $shipping_method_ids ) ) {
$available_shipping_methods = WC()->shipping->load_shipping_methods();
foreach ( $shipping_method_ids as $index => $shipping_method_id ) {
$shipping_method = ( isset( $available_shipping_methods[ $shipping_method_id ] ) && ! empty( $available_shipping_methods[ $shipping_method_id ] ) ) ? $available_shipping_methods[ $shipping_method_id ] : '';
if ( ! empty( $shipping_method ) && is_a( $shipping_method, 'WC_Shipping_Method' ) ) {
$shipping_method_title = is_callable( array( $shipping_method, 'get_method_title' ) ) ? $shipping_method->get_method_title() : '';
if ( ! empty( $shipping_method_title ) ) {
$shipping_method_titles[ $index ] = $shipping_method_title; // Replace shipping method id with it's repective title.
} else {
$shipping_method_titles[ $index ] = $shipping_method->id; // In case of empty shipping method title replace it with method id.
}
}
}
}
return $shipping_method_titles;
}
/**
* Process coupon meta value for import
*
* @param mixed $meta_value The meta value.
* @param array $args Additional Arguments.
* @return mixed $meta_value
*/
public function process_coupon_meta_value_for_import( $meta_value = null, $args = array() ) {
if ( ! empty( $args['meta_key'] ) && 'wc_sc_shipping_method_ids' === $args['meta_key'] ) {
$meta_value = ( ! empty( $args['postmeta']['wc_sc_shipping_method_ids'] ) ) ? explode( '|', wc_clean( wp_unslash( $args['postmeta']['wc_sc_shipping_method_ids'] ) ) ) : array();
if ( is_array( $meta_value ) && ! empty( $meta_value ) ) {
$available_shipping_methods = WC()->shipping->load_shipping_methods();
if ( is_array( $available_shipping_methods ) && ! empty( $available_shipping_methods ) ) {
foreach ( $meta_value as $index => $shipping_method_title ) {
foreach ( $available_shipping_methods as $shipping_method ) {
$method_title = is_callable( array( $shipping_method, 'get_method_title' ) ) ? $shipping_method->get_method_title() : '';
if ( $method_title === $shipping_method_title && ! empty( $shipping_method->id ) ) {
$meta_value[ $index ] = $shipping_method->id; // Replace shipping method title with it's repective id.
}
}
}
}
}
}
return $meta_value;
}
/**
* Function to copy shipping method restriction meta in newly generated coupon
*
* @param array $args The arguments.
*/
public function copy_coupon_shipping_method_meta( $args = array() ) {
// Copy meta data to new coupon.
$this->copy_coupon_meta_data(
$args,
array( 'wc_sc_shipping_method_ids' )
);
}
/**
* Make meta data of shipping method ids protected
*
* @param bool $protected Is protected.
* @param string $meta_key The meta key.
* @param string $meta_type The meta type.
* @return bool $protected
*/
public function make_action_meta_protected( $protected = false, $meta_key = '', $meta_type = '' ) {
if ( 'wc_sc_shipping_method_ids' === $meta_key ) {
return true;
}
return $protected;
}
/**
* WC Blocks on shipping change should trigger auto-apply coupon.
*
* @param string|null $package_id The sanitized ID of the package being updated. Null if all packages are being updated.
* @param string $rate_id The sanitized chosen rate ID for the package.
* @param \WP_REST_Request $request Full details about the request.
*/
public function wc_blocks_sc_shipping_change_auto_apply_coupon( $package_id, $rate_id, $request ) {
$class_auto_apply = WC_SC_Auto_Apply_Coupon::get_instance();
$class_auto_apply->auto_apply_coupons();
}
}
}
WC_SC_Coupons_By_Shipping_Method::get_instance();

View File

@@ -0,0 +1,880 @@
<?php
/**
* Class to handle feature Coupons By Taxonomy
*
* @author StoreApps
* @category Admin
* @package wocommerce-smart-coupons/includes
* @since 4.13.0
* @version 1.8.0
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
if ( ! class_exists( 'WC_SC_Coupons_By_Taxonomy' ) ) {
/**
* Class WC_SC_Coupons_By_Taxonomy
*/
class WC_SC_Coupons_By_Taxonomy {
/**
* Variable to hold instance of this class
*
* @var $instance
*/
private static $instance = null;
/**
* Constructor
*/
private function __construct() {
add_action( 'woocommerce_coupon_options_usage_restriction', array( $this, 'usage_restriction' ), 99, 2 );
add_action( 'wp_ajax_taxonomy_restriction_row_html', array( $this, 'ajax_taxonomy_restriction_row_html' ) );
add_action( 'wp_ajax_taxonomy_restriction_select_tag_html', array( $this, 'ajax_taxonomy_restriction_select_tag_html' ) );
add_action( 'wp_ajax_default_taxonomy_restriction_row_html', array( $this, 'ajax_default_taxonomy_restriction_row_html' ) );
add_action( 'admin_footer', array( $this, 'styles_and_scripts' ) );
add_action( 'woocommerce_coupon_options_save', array( $this, 'process_meta' ), 10, 2 );
add_filter( 'woocommerce_coupon_is_valid_for_product', array( $this, 'validate' ), 11, 4 );
add_filter( 'woocommerce_coupon_is_valid', array( $this, 'handle_non_product_type_coupons' ), 11, 3 );
add_filter( 'is_protected_meta', array( $this, 'make_action_meta_protected' ), 10, 3 );
add_action( 'wc_sc_new_coupon_generated', array( $this, 'copy_coupon_taxonomy_meta' ) );
add_filter( 'wc_sc_process_coupon_meta_value_for_import', array( $this, 'process_coupon_meta_value_for_import' ), 10, 2 );
add_filter( 'wc_smart_coupons_export_headers', array( $this, 'export_headers' ) );
add_filter( 'sc_generate_coupon_meta', array( $this, 'generate_coupon_meta' ), 10, 2 );
add_filter( 'smart_coupons_parser_postmeta_defaults', array( $this, 'postmeta_defaults' ) );
}
/**
* Get single instance of this class
*
* @return this class Singleton object of this class
*/
public static function get_instance() {
// Check if instance is already exists.
if ( is_null( self::$instance ) ) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Handle call to functions which is not available in this class
*
* @param string $function_name The function name.
* @param array $arguments Array of arguments passed while calling $function_name.
* @return result of function call
*/
public function __call( $function_name = '', $arguments = array() ) {
global $woocommerce_smart_coupon;
if ( ! is_callable( array( $woocommerce_smart_coupon, $function_name ) ) ) {
return;
}
if ( ! empty( $arguments ) ) {
return call_user_func_array( array( $woocommerce_smart_coupon, $function_name ), $arguments );
} else {
return call_user_func( array( $woocommerce_smart_coupon, $function_name ) );
}
}
/**
* Display field for coupon by payment method
*
* @param integer $coupon_id The coupon id.
* @param WC_Coupon $coupon The coupon object.
*/
public function usage_restriction( $coupon_id = 0, $coupon = null ) {
$taxonomy_to_label = $this->get_taxonomy_with_label();
$terms = $this->get_terms_grouped_by_taxonomy();
$operators = array(
'incl' => __( 'Include', 'woocommerce-smart-coupons' ),
'excl' => __( 'Exclude', 'woocommerce-smart-coupons' ),
);
$taxonomy_restrictions = ( $this->is_callable( $coupon, 'get_meta' ) ) ? $coupon->get_meta( 'wc_sc_taxonomy_restrictions' ) : $this->get_post_meta( $coupon_id, 'wc_sc_taxonomy_restrictions', true );
?>
<div class="options_group smart-coupons-field wc_sc_taxonomy_restrictions">
<?php
if ( empty( $taxonomy_restrictions ) ) {
$this->get_default_taxonomy_restriction_row();
}
if ( ! empty( $taxonomy_restrictions ) ) {
$count = count( $taxonomy_restrictions );
for ( $i = 0; $i < $count; $i++ ) {
$args = array(
'index' => $i,
'taxonomy_restriction' => $taxonomy_restrictions[ $i ],
'taxonomy_to_label' => $taxonomy_to_label,
'operators' => $operators,
'terms' => $terms,
);
$this->get_taxonomy_restriction_row( $args );
}
}
?>
</div>
<?php
}
/**
* Get default taxonomy restriction row
*/
public function get_default_taxonomy_restriction_row() {
?>
<p class="form-field">
<label>
<?php
echo esc_html__( 'Taxonomy', 'woocommerce-smart-coupons' );
$tooltip_text = esc_html__( 'Product taxonomies that the coupon will be applicable for, or its availability in the cart in order for the "Fixed cart discount" to be applied, based on whether the taxonomies are included or excluded. All the taxonomies selected here, should be valid, for this coupon to be valid.', 'woocommerce-smart-coupons' );
echo wc_help_tip( $tooltip_text ); // phpcs:ignore
?>
</label>
<span id="wc_sc_add_taxonomy_row" class="button wc_sc_add_taxonomy_row" title="<?php echo esc_attr__( 'Add taxonomy restriction', 'woocommerce-smart-coupons' ); ?>"><?php echo esc_html__( 'Add taxonomy restriction', 'woocommerce-smart-coupons' ); ?></span>
</p>
<?php
}
/**
* Get taxonomy restriction row HTML via AJAX
*/
public function ajax_default_taxonomy_restriction_row_html() {
check_ajax_referer( 'wc-sc-default-taxonomy-restriction-row', 'security' );
$this->get_default_taxonomy_restriction_row();
die();
}
/**
* Get taxonomy restriction row
*
* @param array $args Arguments.
*/
public function get_taxonomy_restriction_row( $args = array() ) {
$index = ( ! empty( $args['index'] ) ) ? absint( $args['index'] ) : 0;
$taxonomy_restriction = ( ! empty( $args['taxonomy_restriction'] ) ) ? $args['taxonomy_restriction'] : array();
$taxonomy_to_label = ( ! empty( $args['taxonomy_to_label'] ) ) ? $args['taxonomy_to_label'] : array();
$terms = ( ! empty( $args['terms'] ) ) ? $args['terms'] : array();
$operators = ( ! empty( $args['operators'] ) ) ? $args['operators'] : array();
$tax = $taxonomy_restriction['tax'];
$op = $taxonomy_restriction['op'];
$value = $taxonomy_restriction['val'];
?>
<p class="form-field wc_sc_taxonomy_restrictions_row wc_sc_taxonomy_restrictions-<?php echo esc_attr( $index ); ?>">
<label>
<?php
if ( 0 === $index ) {
echo esc_html__( 'Taxonomy', 'woocommerce-smart-coupons' );
$tooltip_text = esc_html__( 'Product taxonomies that the coupon will be applicable for, or its availability in the cart in order for the "Fixed cart discount" to be applied, based on whether the taxonomies are included or excluded.', 'woocommerce-smart-coupons' );
echo wc_help_tip( $tooltip_text ); // phpcs:ignore
} else {
echo esc_html( ' ' );
}
?>
</label>
<?php
$args = array(
'index' => $index,
'column' => 'tax',
'all' => $taxonomy_to_label,
'selected' => $tax,
'width' => '140px',
);
$this->get_taxonomy_restriction_select_tag( $args );
?>
<?php
$args = array(
'index' => $index,
'column' => 'op',
'all' => $operators,
'selected' => $op,
'width' => '70px',
);
$this->get_taxonomy_restriction_select_tag( $args );
?>
<?php
$args = array(
'index' => $index,
'column' => 'val',
'all' => $terms[ $tax ],
'selected' => $value,
);
$this->get_taxonomy_restriction_select_tag( $args );
?>
<span id="wc_sc_add_taxonomy_row" class="button wc_sc_add_taxonomy_row dashicons dashicons-plus-alt2" title="<?php echo esc_attr__( 'Add taxonomy restriction', 'woocommerce-smart-coupons' ); ?>"></span><span id="remove_wc_sc_taxonomy_restrictions-<?php echo esc_attr( $index ); ?>" class="button remove_wc_sc_taxonomy_restrictions-<?php echo esc_attr( $index ); ?> dashicons dashicons-no-alt" title="<?php echo esc_attr__( 'Remove taxonomy restriction', 'woocommerce-smart-coupons' ); ?>"></span>
</p>
<?php
}
/**
* Draw taxonomy restriction select tag
*
* @param array $args Arguments.
*/
public function get_taxonomy_restriction_select_tag( $args = array() ) {
$index = ( ! empty( $args['index'] ) ) ? $args['index'] : 0;
$column = ( ! empty( $args['column'] ) ) ? $args['column'] : '';
$all = ( ! empty( $args['all'] ) ) ? $args['all'] : array();
$selected = ( ! empty( $args['selected'] ) ) ? $args['selected'] : array();
$width = ( ! empty( $args['width'] ) ) ? $args['width'] : '350px';
?>
<select name="wc_sc_taxonomy_restrictions[<?php echo esc_attr( $index ); ?>][<?php echo esc_attr( $column ); ?>]<?php echo esc_attr( 'val' === $column ? '[]' : '' ); ?>" id="wc_sc_taxonomy_restrictions-<?php echo esc_attr( $index ); ?>-<?php echo esc_attr( $column ); ?>" style="min-width: <?php echo esc_attr( $width ); ?>;" class="wc-enhanced-select" <?php echo ( 'val' === $column ? 'multiple="multiple"' : '' ); ?> tabindex="-1" aria-hidden="true">
<?php
foreach ( $all as $key => $val ) {
?>
<option value="<?php echo esc_attr( $key ); ?>"
<?php
if ( is_array( $selected ) ) {
selected( in_array( (string) $key, $selected, true ), true );
} else {
selected( $selected, (string) $key );
}
?>
><?php echo esc_html( ucfirst( $val ) ); ?></option>
<?php
}
?>
</select>
<?php
}
/**
* Get taxonomy restriction row HTML via AJAX
*/
public function ajax_taxonomy_restriction_select_tag_html() {
check_ajax_referer( 'wc-sc-taxonomy-restriction-select-tag', 'security' );
$index = ( ! empty( $_POST['index'] ) ) ? sanitize_text_field( wp_unslash( $_POST['index'] ) ) : 0;
$tax = ( ! empty( $_POST['tax'] ) ) ? sanitize_text_field( wp_unslash( $_POST['tax'] ) ) : '';
$terms = $this->get_terms_grouped_by_taxonomy();
$args = array(
'index' => $index,
'column' => 'val',
'all' => $terms[ $tax ],
);
$this->get_taxonomy_restriction_select_tag( $args );
die();
}
/**
* Get taxonomy restriction row HTML via AJAX
*/
public function ajax_taxonomy_restriction_row_html() {
check_ajax_referer( 'wc-sc-taxonomy-restriction-row', 'security' );
$index = ( ! empty( $_POST['index'] ) ) ? sanitize_text_field( wp_unslash( $_POST['index'] ) ) : 0;
$taxonomy_to_label = $this->get_taxonomy_with_label();
$terms = $this->get_terms_grouped_by_taxonomy();
$operators = array(
'incl' => __( 'Include', 'woocommerce-smart-coupons' ),
'excl' => __( 'Exclude', 'woocommerce-smart-coupons' ),
);
$tax = current( array_keys( $taxonomy_to_label ) );
$op = current( array_keys( $operators ) );
$args = array(
'index' => $index,
'taxonomy_restriction' => array(
'tax' => $tax,
'op' => $op,
'val' => array(),
),
'taxonomy_to_label' => $taxonomy_to_label,
'operators' => $operators,
'terms' => $terms,
);
$this->get_taxonomy_restriction_row( $args );
die();
}
/**
* Styles and scripts
*/
public function styles_and_scripts() {
if ( ! wp_script_is( 'jquery' ) ) {
wp_enqueue_script( 'jquery' );
}
?>
<style type="text/css">
.options_group.wc_sc_taxonomy_restrictions span.select2.select2-container {
margin: 0 1em 1em 0;
}
.options_group.wc_sc_taxonomy_restrictions .button {
margin: 0 0 0.5em 0.5em;
}
.options_group.wc_sc_taxonomy_restrictions .button.dashicons {
width: auto;
}
.options_group.wc_sc_taxonomy_restrictions .dashicons.dashicons-no-alt::before {
opacity: 0.5;
font-weight: 100;
}
.options_group.wc_sc_taxonomy_restrictions .dashicons {
vertical-align: middle;
}
.options_group.wc_sc_taxonomy_restrictions .woocommerce-help-tip {
float: right;
margin: 0 0.4em;
transform: translateY(25%);
}
</style>
<script type="text/javascript">
jQuery(function(){
function wc_sc_taxonomy_restrictions_field_loaded() {
jQuery('.wc_sc_taxonomy_restrictions').find('.wc_sc_add_taxonomy_row').filter(':not(:last)').remove();
}
function wc_sc_add_taxonomy_clicked() {
jQuery('.wc_sc_taxonomy_restrictions').find('.wc_sc_add_taxonomy_row').remove();
jQuery('.wc_sc_taxonomy_restrictions').find('p.form-field').filter(':not(p.wc_sc_taxonomy_restrictions_row)').remove();
}
wc_sc_taxonomy_restrictions_field_loaded();
jQuery('body').on('click', '#wc_sc_add_taxonomy_row', function(){
var count = jQuery('.wc_sc_taxonomy_restrictions').find('.wc_sc_taxonomy_restrictions_row').length;
jQuery.ajax({
url: '<?php echo esc_url( admin_url( 'admin-ajax.php' ) ); ?>',
type: 'post',
dataType: 'html',
data: {
action: 'taxonomy_restriction_row_html',
security: '<?php echo esc_html( wp_create_nonce( 'wc-sc-taxonomy-restriction-row' ) ); ?>',
index: count
},
success: function( response ){
if ( response != undefined && response != '' ) {
wc_sc_add_taxonomy_clicked();
jQuery('.wc_sc_taxonomy_restrictions').append(response);
jQuery(document.body).trigger('wc-enhanced-select-init');
if ( count <= 0 ) {
jQuery(document.body).trigger('init_tooltips');
}
}
}
});
});
jQuery('body').on('change', '[id^="wc_sc_taxonomy_restrictions-"][id$="-tax"]', function(){
var current_element = jQuery(this);
var element_id = current_element.attr('id');
var selected_value = current_element.val();
var index = element_id.replace('wc_sc_taxonomy_restrictions-', '').replace('-tax', '');
jQuery.ajax({
url: '<?php echo esc_url( admin_url( 'admin-ajax.php' ) ); ?>',
type: 'post',
dataType: 'html',
data: {
action: 'taxonomy_restriction_select_tag_html',
security: '<?php echo esc_html( wp_create_nonce( 'wc-sc-taxonomy-restriction-select-tag' ) ); ?>',
index: index,
tax: selected_value
},
success: function( response ){
if ( response != undefined && response != '' ) {
jQuery('#wc_sc_taxonomy_restrictions-'+index+'-val').selectWoo('destroy').remove();
jQuery('#wc_sc_taxonomy_restrictions-'+index+'-op').parent().append(response);
jQuery(document.body).trigger('wc-enhanced-select-init');
}
}
});
});
jQuery('body').on('click', '[id^="remove_wc_sc_taxonomy_restrictions-"]', function(){
var current_element = jQuery(this);
if ( current_element.parent().find( '#wc_sc_add_taxonomy_row' ).length > 0 ) {
current_element.parent().prev().find('[id^=remove_wc_sc_taxonomy_restrictions-]').before(current_element.parent().find('#wc_sc_add_taxonomy_row'));
}
if ( current_element.parent().find( '.woocommerce-help-tip' ).length > 0 ) {
current_element.parent().next().find('label').replaceWith(current_element.parent().find('label'));
}
current_element.parent().find('.wc-enhanced-select').selectWoo('destroy');
current_element.parent().remove();
var count = jQuery('.wc_sc_taxonomy_restrictions').find('.wc_sc_taxonomy_restrictions_row').length;
if ( count <= 0 ) {
jQuery.ajax({
url: '<?php echo esc_url( admin_url( 'admin-ajax.php' ) ); ?>',
type: 'post',
dataType: 'html',
data: {
action: 'default_taxonomy_restriction_row_html',
security: '<?php echo esc_html( wp_create_nonce( 'wc-sc-default-taxonomy-restriction-row' ) ); ?>'
},
success: function( response ){
if ( response != undefined && response != '' ) {
jQuery('.wc_sc_taxonomy_restrictions').append(response);
jQuery(document.body).trigger('wc-enhanced-select-init');
jQuery(document.body).trigger('init_tooltips');
}
}
});
}
});
});
</script>
<?php
}
/**
* Get taxonomy with label
*
* @return array
*/
public function get_taxonomy_with_label() {
global $wp_taxonomies;
$taxonomy_to_label = array();
$include_taxonomy = array(
'product_type',
'product_visibility',
'product_tag',
'product_shipping_class',
);
$include_taxonomy = apply_filters( 'wc_sc_include_taxonomy_for_restrictions', $include_taxonomy, array( 'source' => $this ) );
if ( ! empty( $wp_taxonomies ) ) {
foreach ( $wp_taxonomies as $taxonomy => $wp_taxonomy ) {
if ( in_array( $taxonomy, $include_taxonomy, true ) ) {
$taxonomy_to_label[ $taxonomy ] = $wp_taxonomy->label;
}
}
}
return $taxonomy_to_label;
}
/**
* Get terms grouped by taxonomy
*
* @return array
*/
public function get_terms_grouped_by_taxonomy() {
$terms_by_taxonomy = array();
$include_taxonomy = array(
'product_type',
'product_visibility',
'product_tag',
'product_shipping_class',
);
$include_taxonomy = apply_filters( 'wc_sc_include_taxonomy_for_restrictions', $include_taxonomy, array( 'source' => $this ) );
$args = array(
'taxonomy' => $include_taxonomy,
'hide_empty' => false,
);
$terms = get_terms( $args );
if ( ! empty( $terms ) ) {
foreach ( $terms as $term ) {
if ( empty( $terms_by_taxonomy[ $term->taxonomy ] ) || ! is_array( $terms_by_taxonomy[ $term->taxonomy ] ) ) {
$terms_by_taxonomy[ $term->taxonomy ] = array();
}
$terms_by_taxonomy[ $term->taxonomy ][ $term->slug ] = $term->name;
}
}
return $terms_by_taxonomy;
}
/**
* Save coupon by payment method data in meta
*
* @param Integer $post_id The coupon post ID.
* @param WC_Coupon $coupon The coupon object.
*/
public function process_meta( $post_id = 0, $coupon = null ) {
if ( empty( $post_id ) ) {
return;
}
$coupon = new WC_Coupon( $coupon );
$taxonomy_restrictions = ( isset( $_POST['wc_sc_taxonomy_restrictions'] ) ) ? wc_clean( wp_unslash( $_POST['wc_sc_taxonomy_restrictions'] ) ) : array(); // phpcs:ignore
if ( ! empty( $taxonomy_restrictions ) ) {
$taxonomy_restrictions = array_values( $taxonomy_restrictions );
}
if ( $this->is_callable( $coupon, 'update_meta_data' ) && $this->is_callable( $coupon, 'save' ) ) {
$coupon->update_meta_data( 'wc_sc_taxonomy_restrictions', $taxonomy_restrictions );
$coupon->save();
} else {
$this->update_post_meta( $post_id, 'wc_sc_taxonomy_restrictions', $taxonomy_restrictions );
}
}
/**
* Function to validate coupons against taxonomy
*
* @param bool $valid Coupon validity.
* @param WC_Product|null $product Product object.
* @param WC_Coupon|null $coupon Coupon object.
* @param array|null $values Values.
* @return bool $valid
*/
public function validate( $valid = false, $product = null, $coupon = null, $values = null ) {
$backtrace = wp_list_pluck( debug_backtrace( DEBUG_BACKTRACE_IGNORE_ARGS ), 'function' ); // phpcs:ignore
// If coupon is already invalid, no need for further checks.
// Ignore this check if the discount type is a non-product-type discount.
if ( true !== $valid && ! in_array( 'handle_non_product_type_coupons', $backtrace, true ) ) {
return $valid;
}
if ( empty( $product ) || empty( $coupon ) ) {
return $valid;
}
$product_ids = array();
if ( $this->is_wc_gte_30() ) {
$coupon_id = ( is_object( $coupon ) && is_callable( array( $coupon, 'get_id' ) ) ) ? $coupon->get_id() : 0;
$product_ids[] = ( is_object( $product ) && is_callable( array( $product, 'get_id' ) ) ) ? $product->get_id() : 0;
$product_ids[] = ( is_object( $product ) && is_callable( array( $product, 'get_parent_id' ) ) ) ? $product->get_parent_id() : 0;
} else {
$coupon_id = ( ! empty( $coupon->id ) ) ? $coupon->id : 0;
$product_ids[] = ( ! empty( $product->id ) ) ? $product->id : 0;
$product_ids[] = ( is_object( $product ) && is_callable( array( $product, 'get_parent' ) ) ) ? $product->get_parent() : 0;
}
$product_ids = array_unique( array_filter( $product_ids ) );
if ( ! empty( $coupon_id ) ) {
$taxonomy_restrictions = ( $this->is_callable( $coupon, 'get_meta' ) ) ? $coupon->get_meta( 'wc_sc_taxonomy_restrictions' ) : $this->get_post_meta( $coupon_id, 'wc_sc_taxonomy_restrictions', true );
if ( ! empty( $taxonomy_restrictions ) ) {
$term_ids = $this->get_restricted_term_ids( array( 'taxonomy_restrictions' => $taxonomy_restrictions ) );
$include_ids = array();
$exclude_ids = array();
if ( isset( $term_ids['include'] ) && is_array( $term_ids['include'] ) ) {
$include_ids = $term_ids['include'];
}
if ( isset( $term_ids['exclude'] ) && is_array( $term_ids['exclude'] ) ) {
$exclude_ids = $term_ids['exclude'];
}
$taxonomies = wp_list_pluck( $taxonomy_restrictions, 'tax' );
$args = array(
'fields' => 'ids',
);
$object_term_ids = wp_get_object_terms( $product_ids, $taxonomies, $args );
$object_term_ids = array_unique( array_filter( $object_term_ids ) );
$taxonomy_found = true;
if ( ! empty( $include_ids ) && is_array( $include_ids ) ) {
$common_term_ids = array_intersect( $include_ids, $object_term_ids );
if ( count( $common_term_ids ) > 0 ) {
$taxonomy_found = true;
} else {
$taxonomy_found = false;
}
}
$exclude_taxonomy_found = false;
if ( ! empty( $exclude_ids ) && is_array( $exclude_ids ) ) {
$common_exclude_term_ids = array_intersect( $exclude_ids, $object_term_ids );
if ( count( $common_exclude_term_ids ) > 0 ) {
$exclude_taxonomy_found = true;
} else {
$exclude_taxonomy_found = false;
}
}
$valid = ( $taxonomy_found && ! $exclude_taxonomy_found ) ? true : false;
}
}
return $valid;
}
/**
* Function to validate non product type coupons against taxonomy restriction
* We need to remove coupon if it does not pass taxonomy validation even for single cart item in case of non product type coupons e.g fixed_cart, smart_coupon since these coupon type require all products in the cart to be valid
*
* @param boolean $valid Coupon validity.
* @param WC_Coupon $coupon Coupon object.
* @param WC_Discounts $discounts Discounts object.
* @throws Exception Validation exception.
* @return boolean $valid Coupon validity
*/
public function handle_non_product_type_coupons( $valid = true, $coupon = null, $discounts = null ) {
// If coupon is already invalid, no need for further checks.
if ( true !== $valid ) {
return $valid;
}
if ( ! is_a( $coupon, 'WC_Coupon' ) ) {
return $valid;
}
if ( $this->is_wc_gte_30() ) {
$coupon_id = ( is_object( $coupon ) && is_callable( array( $coupon, 'get_id' ) ) ) ? $coupon->get_id() : 0;
$discount_type = ( is_object( $coupon ) && is_callable( array( $coupon, 'get_discount_type' ) ) ) ? $coupon->get_discount_type() : '';
} else {
$coupon_id = ( ! empty( $coupon->id ) ) ? $coupon->id : 0;
$discount_type = ( ! empty( $coupon->discount_type ) ) ? $coupon->discount_type : '';
}
if ( ! empty( $coupon_id ) ) {
$taxonomy_restrictions = ( $this->is_callable( $coupon, 'get_meta' ) ) ? $coupon->get_meta( 'wc_sc_taxonomy_restrictions' ) : $this->get_post_meta( $coupon_id, 'wc_sc_taxonomy_restrictions', true );
// If product attributes are not set in coupon, stop further processing and return from here.
if ( empty( $taxonomy_restrictions ) ) {
return $valid;
}
} else {
return $valid;
}
$product_coupon_types = wc_get_product_coupon_types();
// Proceed if it is non product type coupon.
if ( ! in_array( $discount_type, $product_coupon_types, true ) ) {
if ( class_exists( 'WC_Discounts' ) && isset( WC()->cart ) ) {
$wc_cart = WC()->cart;
$wc_discounts = new WC_Discounts( $wc_cart );
$items_to_validate = array();
if ( is_callable( array( $wc_discounts, 'get_items_to_validate' ) ) ) {
$items_to_validate = $wc_discounts->get_items_to_validate();
} elseif ( is_callable( array( $wc_discounts, 'get_items' ) ) ) {
$items_to_validate = $wc_discounts->get_items();
} elseif ( isset( $wc_discounts->items ) && is_array( $wc_discounts->items ) ) {
$items_to_validate = $wc_discounts->items;
}
if ( ! empty( $items_to_validate ) && is_array( $items_to_validate ) ) {
$term_ids = $this->get_restricted_term_ids( array( 'taxonomy_restrictions' => $taxonomy_restrictions ) );
$include_ids = array();
$exclude_ids = array();
if ( isset( $term_ids['include'] ) && is_array( $term_ids['include'] ) ) {
$include_ids = $term_ids['include'];
}
if ( isset( $term_ids['exclude'] ) && is_array( $term_ids['exclude'] ) ) {
$exclude_ids = $term_ids['exclude'];
}
$taxonomies = wp_list_pluck( $taxonomy_restrictions, 'tax' );
$valid_products = array();
$invalid_products = array();
foreach ( $items_to_validate as $item ) {
$cart_item = clone $item; // Clone the item so changes to wc_discounts item do not affect the originals.
$item_product = isset( $cart_item->product ) ? $cart_item->product : null;
$item_object = isset( $cart_item->object ) ? $cart_item->object : null;
if ( ! is_null( $item_product ) && ! is_null( $item_object ) ) {
if ( $coupon->is_valid_for_product( $item_product, $item_object ) ) {
$valid_products[] = $item_product;
} else {
$invalid_products[] = $item_product;
}
}
}
// If cart does not have any valid product then throw Exception.
if ( 0 === count( $valid_products ) ) {
$error_message = __( 'Sorry, this coupon is not applicable to selected products.', 'woocommerce-smart-coupons' );
$error_code = defined( 'E_WC_COUPON_NOT_APPLICABLE' ) ? E_WC_COUPON_NOT_APPLICABLE : 0;
throw new Exception( $error_message, $error_code );
} elseif ( count( $invalid_products ) > 0 && ! empty( $exclude_ids ) ) {
$excluded_products = array();
foreach ( $invalid_products as $invalid_product ) {
$product_ids = array();
$product_ids[] = ( is_object( $invalid_product ) && is_callable( array( $invalid_product, 'get_id' ) ) ) ? $invalid_product->get_id() : 0;
$product_ids[] = ( is_object( $invalid_product ) && is_callable( array( $invalid_product, 'get_parent_id' ) ) ) ? $invalid_product->get_parent_id() : 0;
$product_name = ( is_object( $invalid_product ) && is_callable( array( $invalid_product, 'get_name' ) ) ) ? $invalid_product->get_name() : '';
$args = array(
'fields' => 'ids',
);
$object_term_ids = wp_get_object_terms( $product_ids, $taxonomies, $args );
$object_term_ids = array_unique( array_filter( $object_term_ids ) );
if ( ! empty( $object_term_ids ) && is_array( $object_term_ids ) ) {
$common_exclude_term_ids = array_intersect( $exclude_ids, $object_term_ids );
if ( count( $common_exclude_term_ids ) > 0 ) {
$excluded_products[] = $product_name;
}
}
}
if ( count( $excluded_products ) > 0 ) {
// If cart contains any excluded product and it is being excluded from our excluded product attributes then throw Exception.
/* translators: 1. Singular/plural label for product(s) 2. Excluded product names */
$error_message = sprintf( __( 'Sorry, this coupon is not applicable to the %1$s: %2$s.', 'woocommerce-smart-coupons' ), _n( 'product', 'products', count( $excluded_products ), 'woocommerce-smart-coupons' ), implode( ', ', $excluded_products ) );
$error_code = defined( 'E_WC_COUPON_EXCLUDED_PRODUCTS' ) ? E_WC_COUPON_EXCLUDED_PRODUCTS : 0;
throw new Exception( $error_message, $error_code );
}
}
}
}
}
return $valid;
}
/**
* Get restricted term ids
*
* @param array $args Arguments.
* @return array
*/
public function get_restricted_term_ids( $args = array() ) {
global $wp_taxonomies;
$term_ids = array();
$taxonomy_restrictions = ( ! empty( $args['taxonomy_restrictions'] ) ) ? $args['taxonomy_restrictions'] : array();
if ( ! empty( $taxonomy_restrictions ) && is_array( $taxonomy_restrictions ) ) {
$include_ids = array();
$exclude_ids = array();
foreach ( $taxonomy_restrictions as $taxonomy_restriction ) {
$taxonomy = ( ! empty( $taxonomy_restriction['tax'] ) ) ? $taxonomy_restriction['tax'] : '';
$operator = ( ! empty( $taxonomy_restriction['op'] ) ) ? $taxonomy_restriction['op'] : '';
$value = ( ! empty( $taxonomy_restriction['val'] ) ) ? $taxonomy_restriction['val'] : array();
if ( ! empty( $taxonomy ) && ! empty( $operator ) && ! empty( $value ) ) {
$args = array(
'taxonomy' => $taxonomy,
'hide_empty' => false,
'fields' => 'ids',
'slug' => $value,
);
$found_ids = get_terms( $args );
$found_ids = array_unique( array_filter( $found_ids ) );
if ( ! empty( $found_ids ) ) {
switch ( $operator ) {
case 'incl':
$include_ids = array_merge( $include_ids, $found_ids );
break;
case 'excl':
$exclude_ids = array_merge( $exclude_ids, $found_ids );
break;
}
}
}
}
if ( ! empty( $include_ids ) ) {
$term_ids['include'] = array_unique( array_filter( $include_ids ) );
}
if ( ! empty( $exclude_ids ) ) {
$term_ids['exclude'] = array_unique( array_filter( $exclude_ids ) );
}
}
return $term_ids;
}
/**
* Function to copy taxonomy restriction meta in newly generated coupon
*
* @param array $args The arguments.
*/
public function copy_coupon_taxonomy_meta( $args = array() ) {
// Copy meta data to new coupon.
$this->copy_coupon_meta_data(
$args,
array( 'wc_sc_taxonomy_restrictions' )
);
}
/**
* Make meta data of wc_sc_taxonomy_restrictions protected
*
* @param bool $protected Is protected.
* @param string $meta_key The meta key.
* @param string $meta_type The meta type.
* @return bool $protected
*/
public function make_action_meta_protected( $protected = false, $meta_key = '', $meta_type = '' ) {
if ( 'wc_sc_taxonomy_restrictions' === $meta_key ) {
return true;
}
return $protected;
}
/**
* Add taxonomy restriction meta with value in coupon meta
*
* @param array $data The row data.
* @param array $post The POST values.
* @return array Modified data
*/
public function generate_coupon_meta( $data = array(), $post = array() ) {
if ( isset( $post['wc_sc_taxonomy_restrictions'] ) && is_array( $post['wc_sc_taxonomy_restrictions'] ) && ! empty( $post['wc_sc_taxonomy_restrictions'] ) ) {
$data['wc_sc_taxonomy_restrictions'] = maybe_serialize( array_values( $post['wc_sc_taxonomy_restrictions'] ) );
}
return $data;
}
/**
* Post meta defaults for product quantity restriction meta
*
* @param array $defaults Existing postmeta defaults.
* @return array
*/
public function postmeta_defaults( $defaults = array() ) {
return array_merge( $defaults, array( 'wc_sc_taxonomy_restrictions' => '' ) );
}
/**
* Process coupon meta value for import
*
* @param mixed $meta_value The meta value.
* @param array $args Additional Arguments.
* @return mixed $meta_value
*/
public function process_coupon_meta_value_for_import( $meta_value = null, $args = array() ) {
return ( ! empty( $args['meta_key'] ) && 'wc_sc_taxonomy_restrictions' === $args['meta_key'] && ! empty( $args['postmeta'] ) && ! empty( $args['postmeta']['wc_sc_taxonomy_restrictions'] ) ) ? $args['postmeta']['wc_sc_taxonomy_restrictions'] : $meta_value;
}
/**
* Add wc_sc_taxonomy_restrictions meta in export headers
*
* @param array $headers Existing headers.
* @return array
*/
public function export_headers( $headers = array() ) {
$product_wc_sc_taxonomy_restrictions = array(
'wc_sc_taxonomy_restrictions' => __( 'Taxonomy based restrictions', 'woocommerce-smart-coupons' ),
);
return array_merge( $headers, $product_wc_sc_taxonomy_restrictions );
}
}
}
WC_SC_Coupons_By_Taxonomy::get_instance();

View File

@@ -0,0 +1,547 @@
<?php
/**
* Class to handle feature Coupons By User Role
*
* @author StoreApps
* @category Admin
* @package wocommerce-smart-coupons/includes
* @version 2.1.0
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
if ( ! class_exists( 'WC_SC_Coupons_By_User_Role' ) ) {
/**
* Class WC_SC_Coupons_By_User_Role
*/
class WC_SC_Coupons_By_User_Role {
/**
* Variable to hold instance of this class
*
* @var $instance
*/
private static $instance = null;
/**
* Constructor
*/
public function __construct() {
add_action( 'woocommerce_coupon_options_usage_restriction', array( $this, 'usage_restriction' ), 10, 2 );
add_action( 'woocommerce_coupon_options_save', array( $this, 'process_meta' ), 10, 2 );
add_filter( 'woocommerce_coupon_is_valid', array( $this, 'validate' ), 11, 3 );
add_action( 'woocommerce_after_checkout_validation', array( $this, 'validate_after_checkout' ), 99, 2 );
add_filter( 'wc_smart_coupons_export_headers', array( $this, 'export_headers' ) );
add_filter( 'wc_sc_export_coupon_meta', array( $this, 'export_coupon_meta_data' ), 10, 2 );
add_filter( 'smart_coupons_parser_postmeta_defaults', array( $this, 'postmeta_defaults' ) );
add_filter( 'sc_generate_coupon_meta', array( $this, 'generate_coupon_meta' ), 10, 2 );
add_filter( 'wc_sc_process_coupon_meta_value_for_import', array( $this, 'process_coupon_meta_value_for_import' ), 10, 2 );
add_filter( 'is_protected_meta', array( $this, 'make_action_meta_protected' ), 10, 3 );
add_action( 'wc_sc_new_coupon_generated', array( $this, 'copy_coupon_user_role_meta' ) );
}
/**
* Get single instance of this class
*
* @return this class Singleton object of this class
*/
public static function get_instance() {
// Check if instance is already exists.
if ( is_null( self::$instance ) ) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Handle call to functions which is not available in this class
*
* @param string $function_name The function name.
* @param array $arguments Array of arguments passed while calling $function_name.
* @return result of function call
*/
public function __call( $function_name = '', $arguments = array() ) {
global $woocommerce_smart_coupon;
if ( ! is_callable( array( $woocommerce_smart_coupon, $function_name ) ) ) {
return;
}
if ( ! empty( $arguments ) ) {
return call_user_func_array( array( $woocommerce_smart_coupon, $function_name ), $arguments );
} else {
return call_user_func( array( $woocommerce_smart_coupon, $function_name ) );
}
}
/**
* Display field for coupon by user role
*
* @param integer $coupon_id The coupon id.
* @param WC_Coupon $coupon The coupon object.
*/
public function usage_restriction( $coupon_id = 0, $coupon = null ) {
$user_role_ids = array();
$exclude_user_role_ids = array();
if ( ! empty( $coupon_id ) ) {
if ( ! is_a( $coupon, 'WC_Coupon' ) ) {
$coupon = new WC_Coupon( $coupon_id );
}
$is_callable_coupon_get_meta = $this->is_callable( $coupon, 'get_meta' );
$user_role_ids = ( true === $is_callable_coupon_get_meta ) ? $coupon->get_meta( 'wc_sc_user_role_ids' ) : get_post_meta( $coupon_id, 'wc_sc_user_role_ids', true );
if ( empty( $user_role_ids ) || ! is_array( $user_role_ids ) ) {
$user_role_ids = array();
}
$exclude_user_role_ids = ( true === $is_callable_coupon_get_meta ) ? $coupon->get_meta( 'wc_sc_exclude_user_role_ids' ) : get_post_meta( $coupon_id, 'wc_sc_exclude_user_role_ids', true );
if ( empty( $exclude_user_role_ids ) || ! is_array( $exclude_user_role_ids ) ) {
$exclude_user_role_ids = array();
}
}
$available_user_roles = $this->get_available_user_roles();
?>
<div class="options_group smart-coupons-field">
<p class="form-field">
<label for="wc_sc_user_role_ids"><?php echo esc_html__( 'Allowed user roles', 'woocommerce-smart-coupons' ); ?></label>
<select id="wc_sc_user_role_ids" name="wc_sc_user_role_ids[]" style="width: 50%;" class="wc-enhanced-select" multiple="multiple" data-placeholder="<?php esc_attr_e( 'No user roles', 'woocommerce-smart-coupons' ); ?>">
<?php
if ( ! empty( $available_user_roles ) && is_array( $available_user_roles ) ) {
foreach ( $available_user_roles as $role_id => $role ) {
$role_name = translate_user_role( $role['name'] );
echo '<option value="' . esc_attr( $role_id ) . '"' . esc_attr( selected( in_array( $role_id, $user_role_ids, true ), true, false ) ) . '>' . esc_html( $role_name ) . '</option>';
}
}
?>
</select>
<?php
$tooltip_text = esc_html__( 'Role of the users for whom this coupon is valid. Keep empty if you want this coupon to be valid for users with any role.', 'woocommerce-smart-coupons' );
echo wc_help_tip( $tooltip_text ); // phpcs:ignore
?>
</p>
<p class="form-field">
<label for="wc_sc_exclude_user_role_ids"><?php echo esc_html__( 'Exclude user roles', 'woocommerce-smart-coupons' ); ?></label>
<select id="wc_sc_exclude_user_role_ids" name="wc_sc_exclude_user_role_ids[]" style="width: 50%;" class="wc-enhanced-select" multiple="multiple" data-placeholder="<?php esc_attr_e( 'No user roles', 'woocommerce-smart-coupons' ); ?>">
<?php
if ( ! empty( $available_user_roles ) && is_array( $available_user_roles ) ) {
foreach ( $available_user_roles as $role_id => $role ) {
$role_name = translate_user_role( $role['name'] );
echo '<option value="' . esc_attr( $role_id ) . '"' . esc_attr( selected( in_array( $role_id, $exclude_user_role_ids, true ), true, false ) ) . '>' . esc_html( $role_name ) . '</option>';
}
}
?>
</select>
<?php
$tooltip_text = esc_html__( 'Role of the users for whom this coupon is not valid. Keep empty if you want this coupon to be valid for users with any role.', 'woocommerce-smart-coupons' );
echo wc_help_tip( $tooltip_text ); // phpcs:ignore
?>
</p>
</div>
<?php
}
/**
* Save coupon by user role data in meta
*
* @param Integer $post_id The coupon post ID.
* @param WC_Coupon $coupon The coupon object.
*/
public function process_meta( $post_id = 0, $coupon = null ) {
if ( empty( $post_id ) ) {
return;
}
$coupon = new WC_Coupon( $coupon );
$user_role_ids = ( isset( $_POST['wc_sc_user_role_ids'] ) ) ? wc_clean( wp_unslash( $_POST['wc_sc_user_role_ids'] ) ) : array(); // phpcs:ignore
$exclude_user_role_ids = ( isset( $_POST['wc_sc_exclude_user_role_ids'] ) ) ? wc_clean( wp_unslash( $_POST['wc_sc_exclude_user_role_ids'] ) ) : array(); // phpcs:ignore
if ( $this->is_callable( $coupon, 'update_meta_data' ) && $this->is_callable( $coupon, 'save' ) ) {
$coupon->update_meta_data( 'wc_sc_user_role_ids', $user_role_ids );
$coupon->update_meta_data( 'wc_sc_exclude_user_role_ids', $exclude_user_role_ids );
$coupon->save();
} else {
update_post_meta( $post_id, 'wc_sc_user_role_ids', $user_role_ids );
update_post_meta( $post_id, 'wc_sc_exclude_user_role_ids', $exclude_user_role_ids );
}
}
/**
* Validate the coupon based on user role
*
* @param boolean $valid Is valid or not.
* @param WC_Coupon $coupon The coupon object.
* @param WC_Discounts $discounts The discount object.
*
* @throws Exception If the coupon is invalid.
* @return boolean Is valid or not
*/
public function validate( $valid = false, $coupon = object, $discounts = null ) {
// If coupon is invalid already, no need for further checks.
if ( false === $valid ) {
return $valid;
}
$coupon_id = ( $this->is_wc_gte_30() ) ? $coupon->get_id() : $coupon->id;
if ( ! is_a( $coupon, 'WC_Coupon' ) ) {
$coupon = new WC_Coupon( $coupon_id );
}
if ( $this->is_callable( $coupon, 'get_meta' ) ) {
$user_role_ids = $coupon->get_meta( 'wc_sc_user_role_ids' );
$exclude_user_role_ids = $coupon->get_meta( 'wc_sc_exclude_user_role_ids' );
} else {
$user_role_ids = get_post_meta( $coupon_id, 'wc_sc_user_role_ids', true );
$exclude_user_role_ids = get_post_meta( $coupon_id, 'wc_sc_exclude_user_role_ids', true );
}
$current_user = wp_get_current_user();
$post_action = ( ! empty( $_POST['action'] ) ) ? wc_clean( wp_unslash( $_POST['action'] ) ) : ''; // phpcs:ignore
if ( is_admin() && wp_doing_ajax() && 'woocommerce_add_coupon_discount' === $post_action ) { // This condition will allow the addition of coupon from admin side, in the order even if the user role is not matching.
return true;
}
if ( is_array( $user_role_ids ) && ! empty( $user_role_ids ) ) {
// Check if current user's role is allowed.
if ( ! array_intersect( $current_user->roles, $user_role_ids ) ) {
throw new Exception( __( 'This coupon is not valid for you.', 'woocommerce-smart-coupons' ) );
}
}
if ( is_array( $exclude_user_role_ids ) && ! empty( $exclude_user_role_ids ) ) {
// Check if current user's role is excluded.
if ( array_intersect( $current_user->roles, $exclude_user_role_ids ) ) {
throw new Exception( __( 'This coupon is not valid for you.', 'woocommerce-smart-coupons' ) );
}
}
return $valid;
}
/**
* Add meta in export headers
*
* @param array $headers Existing headers.
* @return array
*/
public function export_headers( $headers = array() ) {
$headers['wc_sc_user_role_ids'] = __( 'User Role', 'woocommerce-smart-coupons' );
$headers['wc_sc_exclude_user_role_ids'] = __( 'Exclude User Role', 'woocommerce-smart-coupons' );
return $headers;
}
/**
* Function to handle coupon meta data during export of existing coupons
*
* @param mixed $meta_value The meta value.
* @param array $args Additional arguments.
* @return string Processed meta value
*/
public function export_coupon_meta_data( $meta_value = '', $args = array() ) {
if ( ! empty( $args['meta_key'] ) ) {
if ( 'wc_sc_user_role_ids' === $args['meta_key'] ) {
if ( isset( $args['meta_value'] ) && ! empty( $args['meta_value'] ) ) {
$user_role_ids = maybe_unserialize( stripslashes( $args['meta_value'] ) );
if ( is_array( $user_role_ids ) && ! empty( $user_role_ids ) ) {
$user_role_names = $this->get_user_role_names_by_ids( $user_role_ids );
if ( is_array( $user_role_names ) && ! empty( $user_role_names ) ) {
$meta_value = implode( '|', wc_clean( wp_unslash( $user_role_names ) ) ); // Replace user role ids with their respective role name.
}
}
}
} elseif ( 'wc_sc_exclude_user_role_ids' === $args['meta_key'] ) {
if ( isset( $args['meta_value'] ) && ! empty( $args['meta_value'] ) ) {
$exclude_user_role_ids = maybe_unserialize( stripslashes( $args['meta_value'] ) );
if ( is_array( $exclude_user_role_ids ) && ! empty( $exclude_user_role_ids ) ) {
$exclude_user_role_names = $this->get_user_role_names_by_ids( $exclude_user_role_ids );
if ( is_array( $exclude_user_role_names ) && ! empty( $exclude_user_role_names ) ) {
$meta_value = implode( '|', wc_clean( wp_unslash( $exclude_user_role_names ) ) ); // Replace user role ids with their respective role name.
}
}
}
}
}
return $meta_value;
}
/**
* Post meta defaults for user role ids meta
*
* @param array $defaults Existing postmeta defaults.
* @return array
*/
public function postmeta_defaults( $defaults = array() ) {
$defaults['wc_sc_user_role_ids'] = '';
$defaults['wc_sc_exclude_user_role_ids'] = '';
return $defaults;
}
/**
* Add user role's meta with value in coupon meta
*
* @param array $data The row data.
* @param array $post The POST values.
* @return array Modified data
*/
public function generate_coupon_meta( $data = array(), $post = array() ) {
$user_role_names = '';
$exclude_user_role_names = '';
if ( ! empty( $post['wc_sc_user_role_ids'] ) && is_array( $post['wc_sc_user_role_ids'] ) ) {
$user_role_names = $this->get_user_role_names_by_ids( $post['wc_sc_user_role_ids'] );
if ( is_array( $user_role_names ) && ! empty( $user_role_names ) ) {
$user_role_names = implode( '|', wc_clean( wp_unslash( $user_role_names ) ) );
}
}
if ( ! empty( $post['wc_sc_exclude_user_role_ids'] ) && is_array( $post['wc_sc_exclude_user_role_ids'] ) ) {
$exclude_user_role_names = $this->get_user_role_names_by_ids( $post['wc_sc_exclude_user_role_ids'] );
if ( is_array( $exclude_user_role_names ) && ! empty( $exclude_user_role_names ) ) {
$exclude_user_role_names = implode( '|', wc_clean( wp_unslash( $exclude_user_role_names ) ) );
}
}
$data['wc_sc_user_role_ids'] = $user_role_names; // Replace user role ids with their respective role name.
$data['wc_sc_exclude_user_role_ids'] = $exclude_user_role_names; // Replace user role ids with their respective role name.
return $data;
}
/**
* Function to get user role titles for given user role ids
*
* @param array $user_role_ids ids of user roles.
* @return array $user_role_names titles of user roles
*/
public function get_user_role_names_by_ids( $user_role_ids = array() ) {
$user_role_names = array();
if ( is_array( $user_role_ids ) && ! empty( $user_role_ids ) ) {
$available_user_roles = $this->get_available_user_roles();
foreach ( $user_role_ids as $index => $user_role_id ) {
$user_role = ( isset( $available_user_roles[ $user_role_id ] ) && ! empty( $available_user_roles[ $user_role_id ] ) ) ? $available_user_roles[ $user_role_id ] : '';
if ( is_array( $user_role ) && ! empty( $user_role ) ) {
$user_role_name = ! empty( $user_role['name'] ) ? $user_role['name'] : '';
if ( ! empty( $user_role_name ) ) {
$user_role_names[ $index ] = $user_role_name; // Replace user role id with it's repective name.
} else {
$user_role_names[ $index ] = $user_role_id; // In case of empty user role name replace it with role id.
}
}
}
}
return $user_role_names;
}
/**
* Process coupon meta value for import
*
* @param mixed $meta_value The meta value.
* @param array $args Additional Arguments.
* @return mixed $meta_value
*/
public function process_coupon_meta_value_for_import( $meta_value = null, $args = array() ) {
if ( ! empty( $args['meta_key'] ) ) {
$available_user_roles = $this->get_available_user_roles();
if ( 'wc_sc_user_role_ids' === $args['meta_key'] ) {
$meta_value = ( ! empty( $args['postmeta']['wc_sc_user_role_ids'] ) ) ? explode( '|', wc_clean( wp_unslash( $args['postmeta']['wc_sc_user_role_ids'] ) ) ) : array();
if ( is_array( $meta_value ) && ! empty( $meta_value ) ) {
if ( is_array( $available_user_roles ) && ! empty( $available_user_roles ) ) {
foreach ( $meta_value as $index => $user_role_name ) {
foreach ( $available_user_roles as $role_id => $user_role ) {
$role_name = isset( $user_role['name'] ) ? $user_role['name'] : '';
if ( $role_name === $user_role_name ) {
$meta_value[ $index ] = $role_id; // Replace user role title with it's repective id.
}
}
}
}
}
} elseif ( 'wc_sc_exclude_user_role_ids' === $args['meta_key'] ) {
$meta_value = ( ! empty( $args['postmeta']['wc_sc_exclude_user_role_ids'] ) ) ? explode( '|', wc_clean( wp_unslash( $args['postmeta']['wc_sc_exclude_user_role_ids'] ) ) ) : array();
if ( is_array( $meta_value ) && ! empty( $meta_value ) ) {
if ( is_array( $available_user_roles ) && ! empty( $available_user_roles ) ) {
foreach ( $meta_value as $index => $user_role_name ) {
foreach ( $available_user_roles as $role_id => $user_role ) {
$role_name = isset( $user_role['name'] ) ? $user_role['name'] : '';
if ( $role_name === $user_role_name ) {
$meta_value[ $index ] = $role_id; // Replace user role title with it's repective id.
}
}
}
}
}
}
}
return $meta_value;
}
/**
* Make meta data of user role ids protected
*
* @param bool $protected Is protected.
* @param string $meta_key The meta key.
* @param string $meta_type The meta type.
* @return bool $protected
*/
public function make_action_meta_protected( $protected = false, $meta_key = '', $meta_type = '' ) {
if ( in_array( $meta_key, array( 'wc_sc_user_role_ids', 'wc_sc_exclude_user_role_ids' ), true ) ) {
return true;
}
return $protected;
}
/**
* Function to get available user roles which current user use.
*
* @return array $available_user_roles Available user roles
*/
public function get_available_user_roles() {
$available_user_roles = array();
if ( ! function_exists( 'get_editable_roles' ) ) {
require_once ABSPATH . 'wp-admin/includes/user.php';
}
if ( function_exists( 'get_editable_roles' ) ) {
$available_user_roles = get_editable_roles();
}
return $available_user_roles;
}
/**
* Function to copy user role restriction meta in newly generated coupon
*
* @param array $args The arguments.
*/
public function copy_coupon_user_role_meta( $args = array() ) {
// Copy meta data to new coupon.
$this->copy_coupon_meta_data(
$args,
array( 'wc_sc_user_role_ids', 'wc_sc_exclude_user_role_ids' )
);
}
/**
* Validate user role after checkout.
*
* @param array $posted Post data.
* @param WP_Error $errors Validation errors.
* @return void
*/
public function validate_after_checkout( $posted = array(), $errors = object ) {
$current_user_id = get_current_user_id();
if ( ! empty( $current_user_id ) ) {
return;
}
$billing_email = ! empty( $posted['billing_email'] ) ? $posted['billing_email'] : '';
if ( empty( $posted['billing_email'] ) ) {
return;
}
$cart = ( function_exists( 'WC' ) && isset( WC()->cart ) ) ? WC()->cart : null;
if ( is_a( $cart, 'WC_Cart' ) ) {
$is_cart_empty = is_callable( array( $cart, 'is_empty' ) ) && $cart->is_empty();
if ( false === $is_cart_empty ) {
$applied_coupons = ( is_callable( array( $cart, 'get_applied_coupons' ) ) ) ? $cart->get_applied_coupons() : array();
if ( ! empty( $applied_coupons ) ) {
foreach ( $applied_coupons as $code ) {
$coupon = new WC_Coupon( $code );
if ( ! is_object( $coupon ) ) {
continue;
}
if ( is_callable( array( $coupon, 'get_meta' ) ) ) {
$user_role_ids = $coupon->get_meta( 'wc_sc_user_role_ids' );
$exclude_user_role_ids = $coupon->get_meta( 'wc_sc_exclude_user_role_ids' );
} else {
if ( is_callable( array( $coupon, 'get_id' ) ) ) {
$coupon_id = $coupon->get_id();
} else {
$coupon_id = ( ! empty( $coupon->id ) ) ? $coupon->id : 0;
}
if ( empty( $coupon_id ) ) {
continue;
}
$user_role_ids = get_post_meta( $coupon_id, 'wc_sc_user_role_ids', true );
$exclude_user_role_ids = get_post_meta( $coupon_id, 'wc_sc_exclude_user_role_ids', true );
}
if ( empty( $exclude_user_role_ids ) && empty( $user_role_ids ) ) {
continue;
}
$current_user = get_user_by( 'email', $billing_email );
$current_user_roles = ! empty( $current_user->roles ) ? $current_user->roles : array();
$is_message = is_callable( array( $coupon, 'add_coupon_message' ) );
$is_remove = is_callable( array( $cart, 'remove_coupon' ) );
if ( is_array( $user_role_ids ) && ! empty( $user_role_ids ) ) {
// Check if current user's role is allowed.
if ( ! array_intersect( $current_user_roles, $user_role_ids ) ) {
if ( true === $is_message ) {
$coupon->add_coupon_message( WC_Coupon::E_WC_COUPON_NOT_YOURS_REMOVED );
}
if ( true === $is_remove ) {
$cart->remove_coupon( $code );
}
}
}
if ( is_array( $exclude_user_role_ids ) && ! empty( $exclude_user_role_ids ) ) {
// Check if current user's role is excluded.
if ( array_intersect( $current_user_roles, $exclude_user_role_ids ) ) {
if ( true === $is_message ) {
$coupon->add_coupon_message( WC_Coupon::E_WC_COUPON_NOT_YOURS_REMOVED );
}
if ( true === $is_remove ) {
$cart->remove_coupon( $code );
}
}
}
}
}
}
}
}
}
}
WC_SC_Coupons_By_User_Role::get_instance();

View File

@@ -0,0 +1,347 @@
<?php
/**
* Maintain Global Coupon's record
*
* @author StoreApps
* @since 3.3.0
* @version 1.9.0
*
* @package woocommerce-smart-coupons/includes/
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
if ( ! class_exists( 'WC_SC_Global_Coupons' ) ) {
/**
* Class for handling global coupons
*/
class WC_SC_Global_Coupons {
/**
* Variable to hold instance of WC_SC_Global_Coupons
*
* @var $instance
*/
private static $instance = null;
/**
* Constructor
*/
private function __construct() {
add_action( 'admin_init', array( $this, 'set_global_coupons' ) );
add_action( 'woocommerce_coupon_options_save', array( $this, 'update_global_coupons' ), 99, 2 );
add_action( 'deleted_post', array( $this, 'sc_delete_global_coupons' ) );
add_action( 'trashed_post', array( $this, 'sc_delete_global_coupons' ) );
add_action( 'untrashed_post', array( $this, 'sc_untrash_global_coupons' ) );
add_action( 'future_to_publish', array( $this, 'future_to_publish_global_coupons' ) );
}
/**
* Get single instance of WC_SC_Global_Coupons
*
* @return WC_SC_Global_Coupons Singleton object of WC_SC_Global_Coupons
*/
public static function get_instance() {
// Check if instance is already exists.
if ( is_null( self::$instance ) ) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Handle call to functions which is not available in this class
*
* @param string $function_name The function name.
* @param array $arguments Array of arguments passed while calling $function_name.
* @return result of function call
*/
public function __call( $function_name = '', $arguments = array() ) {
global $woocommerce_smart_coupon;
if ( ! is_callable( array( $woocommerce_smart_coupon, $function_name ) ) ) {
return;
}
if ( ! empty( $arguments ) ) {
return call_user_func_array( array( $woocommerce_smart_coupon, $function_name ), $arguments );
} else {
return call_user_func( array( $woocommerce_smart_coupon, $function_name ) );
}
}
/**
* Set global coupons in options table for faster fetching
*/
public function set_global_coupons() {
global $wpdb;
$global_coupon_option_name = 'sc_display_global_coupons';
$global_coupons = $this->sc_get_option( $global_coupon_option_name );
$current_sc_version = get_option( 'sa_sc_db_version', '' ); // code for updating the db - for autoload related fix.
if ( false === $global_coupons ) {
$wpdb->query( $wpdb->prepare( 'SET SESSION group_concat_max_len=%d', 999999 ) ); // phpcs:ignore
$wpdb->delete( $wpdb->prefix . 'options', array( 'option_name' => $global_coupon_option_name ) ); // WPCS: cache ok, db call ok.
$wpdb->query( // phpcs:ignore
$wpdb->prepare(
"REPLACE INTO {$wpdb->prefix}options (option_name, option_value, autoload)
SELECT %s,
IFNULL(GROUP_CONCAT(p.id SEPARATOR ','), ''),
%s
FROM {$wpdb->prefix}posts AS p
JOIN {$wpdb->prefix}postmeta AS pm
ON(pm.post_id = p.ID
AND p.post_type = %s
AND p.post_status = %s
AND pm.meta_key = %s
AND pm.meta_value = %s)
",
$global_coupon_option_name,
'no',
'shop_coupon',
'publish',
'sc_is_visible_storewide',
'yes'
)
);
if ( ! empty( $this->sc_get_option( $global_coupon_option_name ) ) ) {
$wpdb->query( // phpcs:ignore
$wpdb->prepare(
"UPDATE {$wpdb->prefix}options
SET option_value = (SELECT IFNULL(GROUP_CONCAT(DISTINCT pm.post_id SEPARATOR ','), '')
FROM {$wpdb->prefix}postmeta AS pm
WHERE ( ( pm.meta_key = %s AND CAST(pm.meta_value AS CHAR) = %s )
OR NOT EXISTS( SELECT 1 FROM {$wpdb->prefix}postmeta AS pm1 WHERE pm1.meta_key = %s AND pm.post_id = pm1.post_id ) )
AND FIND_IN_SET(pm.post_id, (SELECT option_value FROM (SELECT option_value FROM {$wpdb->prefix}options WHERE option_name = %s) as temp )) > 0 )
WHERE option_name = %s",
'customer_email',
'a:0:{}',
'customer_email',
$global_coupon_option_name,
$global_coupon_option_name
)
);
if ( ! empty( $this->sc_get_option( $global_coupon_option_name ) ) ) {
$wpdb->query( // phpcs:ignore
$wpdb->prepare(
"UPDATE {$wpdb->prefix}options
SET option_value = (SELECT IFNULL(GROUP_CONCAT(post_id SEPARATOR ','), '')
FROM {$wpdb->prefix}postmeta
WHERE meta_key = %s
AND CAST(meta_value AS CHAR) != %s
AND FIND_IN_SET(post_id, (SELECT option_value FROM (SELECT option_value FROM {$wpdb->prefix}options WHERE option_name = %s) as temp )) > 0 )
WHERE option_name = %s",
'auto_generate_coupon',
'yes',
$global_coupon_option_name,
$global_coupon_option_name
)
);
if ( ! empty( $this->sc_get_option( $global_coupon_option_name ) ) ) {
$wpdb->query( // phpcs:ignore
$wpdb->prepare(
"UPDATE {$wpdb->prefix}options
SET option_value = (SELECT IFNULL(GROUP_CONCAT(post_id SEPARATOR ','), '')
FROM {$wpdb->prefix}postmeta
WHERE meta_key = %s
AND CAST(meta_value AS CHAR) != %s
AND FIND_IN_SET(post_id, (SELECT option_value FROM (SELECT option_value FROM {$wpdb->prefix}options WHERE option_name = %s) as temp )) > 0 )
WHERE option_name = %s",
'discount_type',
'smart_coupon',
$global_coupon_option_name,
$global_coupon_option_name
)
);
if ( ! empty( $this->sc_get_option( $global_coupon_option_name ) ) ) {
$wpdb->query( // phpcs:ignore
$wpdb->prepare(
"UPDATE {$wpdb->prefix}options
SET option_value = (SELECT IFNULL(GROUP_CONCAT(DISTINCT pm_expiry_date.post_id SEPARATOR ','), '')
FROM {$wpdb->prefix}postmeta AS pm_expiry_date
JOIN {$wpdb->prefix}postmeta AS pm_expiry_time
on ( pm_expiry_time.post_id = pm_expiry_date.post_id
AND pm_expiry_date.meta_key = %s
AND pm_expiry_time.meta_key = %s
)
WHERE ( (pm_expiry_date.meta_value IS NULL OR pm_expiry_date.meta_value = '')
OR ( IFNULL(pm_expiry_date.meta_value, 0) + IFNULL(pm_expiry_time.meta_value, 0) ) > %d )
AND FIND_IN_SET(pm_expiry_date.post_id, (SELECT option_value FROM (SELECT option_value FROM {$wpdb->prefix}options WHERE option_name = %s) as temp )) > 0 )
WHERE option_name = %s",
'date_expires',
'wc_sc_expiry_time',
time(),
$global_coupon_option_name,
$global_coupon_option_name
)
);
$global_coupons = $this->sc_get_option( $global_coupon_option_name );
}
}
}
}
}
if ( ( empty( $current_sc_version ) || version_compare( $current_sc_version, '3.3.6', '<' ) ) && ! empty( $this->sc_get_option( $global_coupon_option_name ) ) ) {
$wpdb->query( // phpcs:ignore
$wpdb->prepare(
"UPDATE {$wpdb->prefix}options
SET autoload = %s
WHERE option_name = %s",
'no',
$global_coupon_option_name
)
);
$current_sc_version = '3.3.6';
update_option( 'sa_sc_db_version', $current_sc_version, 'no' );
}
}
/**
* Function to update list of global coupons
*
* @param int $post_id The post id.
* @param string $action Action.
* @param WC_Coupon $coupon The coupon object.
*/
public function sc_update_global_coupons( $post_id, $action = 'add', $coupon = null ) {
if ( empty( $post_id ) ) {
return;
}
if ( 'shop_coupon' !== $this->get_post_type( $post_id ) ) {
return;
}
$coupon = new WC_Coupon( $post_id );
$coupon_status = ( $this->is_wc_greater_than( '6.1.2' ) && $this->is_callable( $coupon, 'get_status' ) ) ? $coupon->get_status() : get_post_status( $post_id );
if ( $this->is_callable( $coupon, 'get_meta' ) ) {
$customer_email = $coupon->get_email_restrictions();
$sc_is_visible_storewide = $coupon->get_meta( 'sc_is_visible_storewide' );
$auto_generate_coupon = $coupon->get_meta( 'auto_generate_coupon' );
$discount_type = $coupon->get_discount_type();
} else {
$coupon_meta = get_post_meta( $post_id );
$customer_email = ( ! empty( $coupon_meta['customer_email'] ) ) ? $coupon_meta['customer_email'][0] : '';
$sc_is_visible_storewide = ( ! empty( $coupon_meta['sc_is_visible_storewide'] ) ) ? $coupon_meta['sc_is_visible_storewide'][0] : '';
$auto_generate_coupon = ( ! empty( $coupon_meta['auto_generate_coupon'] ) ) ? $coupon_meta['auto_generate_coupon'][0] : '';
$discount_type = ( ! empty( $coupon_meta['discount_type'] ) ) ? $coupon_meta['discount_type'][0] : '';
}
$global_coupons_list = get_option( 'sc_display_global_coupons' );
$global_coupons = ( ! empty( $global_coupons_list ) ) ? explode( ',', $global_coupons_list ) : array();
$key = array_search( (string) $post_id, $global_coupons, true );
if ( ( 'publish' === $coupon_status
&& ( empty( $customer_email ) || serialize( array() ) === $customer_email ) // phpcs:ignore
&& ( ! empty( $sc_is_visible_storewide ) && 'yes' === $sc_is_visible_storewide )
&& ( ! empty( $auto_generate_coupon ) && 'yes' !== $auto_generate_coupon )
&& ( ! empty( $discount_type ) && 'smart_coupon' !== $discount_type ) )
|| ( 'trash' === $coupon_status && 'delete' === $action ) ) {
if ( 'add' === $action && false === $key ) {
$global_coupons[] = $post_id;
} elseif ( 'delete' === $action && false !== $key ) {
unset( $global_coupons[ $key ] );
}
} else {
if ( false !== $key ) {
unset( $global_coupons[ $key ] );
}
}
update_option( 'sc_display_global_coupons', implode( ',', array_unique( $global_coupons ) ), 'no' );
}
/**
* Function to update list of global coupons on trash / delete coupon
*
* @param int $post_id The post id.
*/
public function sc_delete_global_coupons( $post_id ) {
if ( empty( $post_id ) ) {
return;
}
if ( 'shop_coupon' !== $this->get_post_type( $post_id ) ) {
return;
}
$this->sc_update_global_coupons( $post_id, 'delete' );
}
/**
* Function to update list of global coupons on untrash coupon
*
* @param int $post_id The post id.
*/
public function sc_untrash_global_coupons( $post_id ) {
if ( empty( $post_id ) ) {
return;
}
if ( 'shop_coupon' !== $this->get_post_type( $post_id ) ) {
return;
}
$this->sc_update_global_coupons( $post_id );
}
/**
* Update global coupons data for sheduled coupons
*
* @param WP_Post $post The post object.
*/
public function future_to_publish_global_coupons( $post = null ) {
$post_id = ( ! empty( $post->ID ) ) ? $post->ID : 0;
if ( empty( $post ) || empty( $post_id ) ) {
return;
}
if ( 'shop_coupon' !== $post->post_type ) {
return;
}
$this->sc_update_global_coupons( $post_id );
}
/**
* Update global coupons on saving coupon
*
* @param int $post_id The post id.
* @param WC_Coupon $coupon The coupon object.
*/
public function update_global_coupons( $post_id = 0, $coupon = null ) {
if ( empty( $post_id ) ) {
return;
}
if ( is_null( $coupon ) || ! is_a( $coupon, 'WC_Coupon' ) ) {
$coupon = new WC_Coupon( $post_id );
}
$this->sc_update_global_coupons( $post_id, 'add', $coupon );
}
}
}
WC_SC_Global_Coupons::get_instance();

View File

@@ -0,0 +1,602 @@
<?php
/**
* Smart Coupons fields in orders
*
* @author StoreApps
* @since 3.3.0
* @version 1.8.0
*
* @package woocommerce-smart-coupons/includes/
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
if ( ! class_exists( 'WC_SC_Order_Fields' ) ) {
/**
* Class for handling Smart Coupons' field in orders
*/
class WC_SC_Order_Fields {
/**
* Variable to hold instance of WC_SC_Order_Fields
*
* @var $instance
*/
private static $instance = null;
/**
* Constructor
*/
private function __construct() {
add_action( 'woocommerce_admin_order_totals_after_tax', array( $this, 'admin_order_totals_add_smart_coupons_discount_details' ) );
add_filter( 'woocommerce_get_order_item_totals', array( $this, 'add_smart_coupons_discount_details' ), 10, 3 );
add_action( 'wp_loaded', array( $this, 'order_fields_hooks' ) );
add_filter( 'woocommerce_cart_totals_coupon_label', array( $this, 'cart_totals_smart_coupons_label' ), 10, 2 );
add_filter( 'woocommerce_cart_totals_order_total_html', array( $this, 'cart_totals_order_total_html' ), 99 );
add_filter( 'woocommerce_get_formatted_order_total', array( $this, 'get_formatted_order_total' ), 99, 2 );
add_action( 'woocommerce_email_after_order_table', array( $this, 'show_store_credit_balance' ), 10, 3 );
add_filter( 'woocommerce_get_coupon_id_from_code', array( $this, 'get_coupon_id_from_code' ), 1000, 3 );
add_filter( 'woocommerce_order_item_get_discount', array( $this, 'smart_coupon_get_discount' ), 99, 2 );
}
/**
* Get single instance of WC_SC_Order_Fields
*
* @return WC_SC_Order_Fields Singleton object of WC_SC_Order_Fields
*/
public static function get_instance() {
// Check if instance is already exists.
if ( is_null( self::$instance ) ) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Handle call to functions which is not available in this class
*
* @param string $function_name The function name.
* @param array $arguments Array of arguments passed while calling $function_name.
* @return result of function call
*/
public function __call( $function_name, $arguments = array() ) {
global $woocommerce_smart_coupon;
if ( ! is_callable( array( $woocommerce_smart_coupon, $function_name ) ) ) {
return;
}
if ( ! empty( $arguments ) ) {
return call_user_func_array( array( $woocommerce_smart_coupon, $function_name ), $arguments );
} else {
return call_user_func( array( $woocommerce_smart_coupon, $function_name ) );
}
}
/**
* Function to get total credit used in an order
*
* @param WC_Order $order The order object.
* @return float $total_credit_used
*/
public function get_total_credit_used_in_order( $order = null ) {
if ( empty( $order ) ) {
return 0;
}
$total_credit_used = 0;
if ( ! $this->is_old_sc_order( $order ) ) {
$smart_coupons_contribution = ( $this->is_callable( $order, 'get_meta' ) ) ? $order->get_meta( 'smart_coupons_contribution' ) : array();
if ( ! empty( $smart_coupons_contribution ) ) {
$total_credit_used = array_sum( $smart_coupons_contribution );
}
return $total_credit_used;
}
$coupons = $order->get_items( 'coupon' );
if ( ! empty( $coupons ) ) {
foreach ( $coupons as $item_id => $item ) {
$code = ( is_object( $item ) && is_callable( array( $item, 'get_code' ) ) ) ? $item->get_code() : $item['code'];
if ( empty( $code ) ) {
continue;
}
$coupon = new WC_Coupon( $code );
if ( ! empty( $coupon ) && $coupon instanceof WC_Coupon ) {
if ( $this->is_wc_gte_30() ) {
$coupon_discount_type = ( is_object( $coupon ) && is_callable( array( $coupon, 'get_discount_type' ) ) ) ? $coupon->get_discount_type() : '';
$discount_type = ( ! empty( $coupon_discount_type ) ) ? $coupon_discount_type : '';
} else {
$discount_type = ( ! empty( $coupon->discount_type ) ) ? $coupon->discount_type : '';
}
if ( 'smart_coupon' !== $discount_type ) {
continue;
}
$discount = ( is_object( $item ) && is_callable( array( $item, 'get_discount' ) ) ) ? $item->get_discount() : $item['discount_amount'];
$discount_tax = ( is_object( $item ) && is_callable( array( $item, 'get_discount_tax' ) ) ) ? $item->get_discount_tax() : $item['discount_amount_tax'];
$total_credit_used += $discount;
$sc_include_tax = $this->is_store_credit_include_tax();
// Check if discount include tax.
if ( 'yes' === $sc_include_tax && ! empty( $discount_tax ) ) {
$total_credit_used += $discount_tax;
} else {
$prices_include_tax = ( 'incl' === get_option( 'woocommerce_tax_display_cart' ) ) ? true : false;
if ( true === $prices_include_tax ) {
$apply_before_tax = get_option( 'woocommerce_smart_coupon_apply_before_tax', 'no' );
if ( 'yes' === $apply_before_tax ) {
$_sc_include_tax = get_option( 'woocommerce_smart_coupon_include_tax', 'no' );
if ( 'no' === $_sc_include_tax ) {
$total_credit_used += $discount_tax;
}
}
}
}
}
}
}
return $total_credit_used;
}
/**
* Function to show store credit used in order admin panel
*
* @param int $order_id The order id.
*/
public function admin_order_totals_add_smart_coupons_discount_details( $order_id = 0 ) {
global $store_credit_label;
if ( empty( $order_id ) ) {
return;
}
if ( ! $this->is_old_sc_order( $order_id ) ) {
return;
}
$order = wc_get_order( $order_id );
$total_credit_used = $this->get_total_credit_used_in_order( $order );
if ( empty( $total_credit_used ) ) {
return;
}
?>
<tr>
<td class="label">
<?php
/* translators: %s: singular name for store credit */
echo ! empty( $store_credit_label['singular'] ) ? sprintf( esc_html__( '%s Used', 'woocommerce-smart-coupons' ), esc_html( ucwords( $store_credit_label['singular'] ) ) ) : esc_html__( 'Store Credit Used', 'woocommerce-smart-coupons' );
?>
<span class="tips" data-tip="<?php echo esc_attr__( 'This is the total credit used.', 'woocommerce-smart-coupons' ); ?>">[?]</span>:</td>
<td width="1%"></td>
<td class="total">
<?php
if ( $this->is_wc_gte_30() ) {
echo wc_price( $total_credit_used, array( 'currency' => $order->get_currency() ) ); // phpcs:ignore
} else {
echo wc_price( $total_credit_used, array( 'currency' => $order->get_order_currency() ) ); // phpcs:ignore
}
?>
</td>
</tr>
<?php
}
/**
* Function to add hooks based on conditions
*/
public function order_fields_hooks() {
if ( $this->is_wc_gte_30() ) {
add_filter( 'woocommerce_order_get_total_discount', array( $this, 'smart_coupons_order_amount_total_discount' ), 10, 2 );
} else {
add_filter( 'woocommerce_order_amount_total_discount', array( $this, 'smart_coupons_order_amount_total_discount' ), 10, 2 );
}
}
/**
* Function to add details of discount coming from Smart Coupons
*
* @param array $total_rows All rows.
* @param WC_Order $order The order object.
* @param string $tax_display Tax to display.
* @return array $total_rows
*/
public function add_smart_coupons_discount_details( $total_rows = array(), $order = null, $tax_display = '' ) {
global $store_credit_label;
if ( empty( $order ) ) {
return $total_rows;
}
$total_credit_used = (float) $this->get_total_credit_used_in_order( $order );
$offset = array_search( 'order_total', array_keys( $total_rows ), true );
if ( false !== $offset && ! empty( $total_credit_used ) && ! empty( $total_rows ) ) {
$total_rows = array_merge(
array_slice( $total_rows, 0, $offset ),
array(
'smart_coupon' => array(
/* translators: %s: singular name for store credit */
'label' => ! empty( $store_credit_label['singular'] ) ? sprintf( __( '%s Used:', 'woocommerce-smart-coupons' ), ucwords( $store_credit_label['singular'] ) ) : __( 'Store Credit Used:', 'woocommerce-smart-coupons' ),
'value' => '-' . wc_price( $total_credit_used ),
),
),
array_slice( $total_rows, $offset, null )
);
$total_discount = (float) $order->get_total_discount();
// code to check and manipulate 'Discount' amount based on Store Credit used.
if ( $total_discount === $total_credit_used ) {
unset( $total_rows['discount'] );
} else {
if ( $this->is_old_sc_order( $order ) ) {
$total_discount = $total_discount - $total_credit_used;
$total_rows['discount']['value'] = '-' . wc_price( $total_discount );
} else {
if ( 'incl' === $tax_display ) {
$total_discount_tax = $order->get_discount_tax();
$total_discount += $total_discount_tax;
}
if ( $total_discount > $total_credit_used ) {
$total_discount = $total_discount - $total_credit_used;
if ( ! array_key_exists( 'discount', $total_rows ) ) {
$total_rows['discount'] = array();
}
if ( ! array_key_exists( 'label', $total_rows['discount'] ) ) {
$total_rows['discount']['label'] = __( 'Discount:', 'woocommerce-smart-coupons' );
}
$total_rows['discount']['value'] = '-' . wc_price( $total_discount );
}
}
}
}
return $total_rows;
}
/**
* Function to include discounts from Smart Coupons in total discount of order
*
* @param float $total_discount Total discount.
* @param WC_Order $order The order object.
* @return float $total_discount
*/
public function smart_coupons_order_amount_total_discount( $total_discount, $order = null ) {
if ( ! $this->is_old_sc_order( $order ) ) {
return $total_discount;
}
// To avoid adding store credit in 'Discount' field on order admin panel.
if ( did_action( 'woocommerce_admin_order_item_headers' ) >= 1 ) {
return $total_discount;
}
$total_credit_used = $this->get_total_credit_used_in_order( $order );
if ( $total_credit_used > 0 ) {
$total_discount += $total_credit_used;
}
return $total_discount;
}
/**
* Function to add label for smart_coupons in cart total
*
* @param string $default_label Default label.
* @param WC_Coupon $coupon The coupon object.
* @return string $new_label
*/
public function cart_totals_smart_coupons_label( $default_label = '', $coupon = null ) {
global $store_credit_label;
if ( empty( $coupon ) ) {
return $default_label;
}
if ( $this->is_wc_gte_30() ) {
$discount_type = ( ! empty( $coupon ) && is_callable( array( $coupon, 'get_discount_type' ) ) ) ? $coupon->get_discount_type() : '';
$coupon_code = ( ! empty( $coupon ) && is_callable( array( $coupon, 'get_code' ) ) ) ? $coupon->get_code() : '';
} else {
$discount_type = ( ! empty( $coupon->discount_type ) ) ? $coupon->discount_type : '';
$coupon_code = ( ! empty( $coupon->code ) ) ? $coupon->code : '';
}
if ( ! empty( $discount_type ) && 'smart_coupon' === $discount_type ) {
$credit_label = ! empty( $store_credit_label['singular'] ) ? ucwords( $store_credit_label['singular'] ) . ':' : __( 'Store Credit:', 'woocommerce-smart-coupons' );
return $credit_label . ' ' . $coupon_code;
}
return $default_label;
}
/**
* Modify Tax detail HTML if store credit is applied, in cart
*
* @param string $html The total html.
* @return string $html
*/
public function cart_totals_order_total_html( $html = null ) {
if ( empty( $html ) ) {
return $html;
}
if ( ! class_exists( 'WCS_SC_Compatibility' ) ) {
include_once 'class-wcs-compatibility.php';
}
$is_display_price_incl_tax = ( $this->is_wc_gte_33() ) ? WC()->cart->display_prices_including_tax() : ( 'incl' === WC()->cart->tax_display_cart );
if ( wc_tax_enabled() && true === $is_display_price_incl_tax ) {
$applied_coupons = ( is_object( WC()->cart ) && is_callable( array( WC()->cart, 'get_applied_coupons' ) ) ) ? WC()->cart->get_applied_coupons() : array();
if ( empty( $applied_coupons ) ) {
return $html;
}
foreach ( $applied_coupons as $code ) {
$coupon = new WC_Coupon( $code );
if ( $this->is_wc_gte_30() ) {
$discount_type = $coupon->get_discount_type();
} else {
$discount_type = ( ! empty( $coupon->discount_type ) ) ? $coupon->discount_type : '';
}
if ( ! is_a( $coupon, 'WC_Coupon' ) || 'smart_coupon' !== $discount_type ) {
continue;
}
if ( WC()->cart->get_total() === 0 || WC()->cart->get_total() <= WC()->cart->get_taxes_total() ) {
$cart_contains_subscription = WCS_SC_Compatibility::is_cart_contains_subscription();
$calculation_type = '';
if ( $cart_contains_subscription ) {
$calculation_type = WC_Subscriptions_Cart::get_calculation_type();
if ( 'recurring_total' !== $calculation_type ) {
return '<strong>' . WC()->cart->get_total() . '</strong> ';
} else {
return $html;
}
} else {
return '<strong>' . WC()->cart->get_total() . '</strong> ';
}
}
}
}
return $html;
}
/**
* Modify Tax detail HTML if store credit is applied, in order
*
* @param string $html The order total html.
* @param WC_Order $order The order object (optional).
* @return string $html
*/
public function get_formatted_order_total( $html = null, $order = null ) {
if ( empty( $html ) || empty( $order ) ) {
return $html;
}
if ( $this->is_wc_gte_30() ) {
$tax_display = get_option( 'woocommerce_tax_display_cart' );
} else {
$tax_display = ( ! empty( $order->tax_display_cart ) ) ? $order->tax_display_cart : '';
}
if ( wc_tax_enabled() && 'incl' === $tax_display ) {
$applied_coupons = $this->get_coupon_codes( $order );
if ( empty( $applied_coupons ) ) {
return $html;
}
foreach ( $applied_coupons as $code ) {
$coupon = new WC_Coupon( $code );
if ( $this->is_wc_gte_30() ) {
$discount_type = $coupon->get_discount_type();
} else {
$discount_type = ( ! empty( $coupon->discount_type ) ) ? $coupon->discount_type : '';
}
if ( ! is_a( $coupon, 'WC_Coupon' ) || 'smart_coupon' !== $discount_type ) {
continue;
}
if ( $order->get_total() === 0 || $order->get_total() <= $order->get_total_tax() ) {
return wc_price( $order->get_total() );
}
}
}
return $html;
}
/**
* Function to notify user about remaining balance in Store Credit in "Order Complete" email
*
* @param WC_Order $order The order object.
* @param boolean $send_to_admin Is send to admin.
* @param boolean $plain_text Is plain text email.
*/
public function show_store_credit_balance( $order = false, $send_to_admin = false, $plain_text = false ) {
global $store_credit_label;
if ( $send_to_admin ) {
return;
}
if ( $this->is_wc_gte_30() ) {
$order_refunds = ( ! empty( $order ) && is_callable( array( $order, 'get_refunds' ) ) ) ? $order->get_refunds() : array();
} else {
$order_refunds = ( ! empty( $order->refunds ) ) ? $order->refunds : array();
}
if ( ! empty( $order_refunds ) ) {
return;
}
$used_coupons = $this->get_coupon_codes( $order );
if ( count( $used_coupons ) > 0 ) {
$store_credit_balance = '';
foreach ( $used_coupons as $code ) {
if ( ! $code ) {
continue;
}
$coupon = new WC_Coupon( $code );
if ( $this->is_wc_gte_30() ) {
$discount_type = $coupon->get_discount_type();
$coupon_code = $coupon->get_code();
} else {
$discount_type = ( ! empty( $coupon->discount_type ) ) ? $coupon->discount_type : '';
$coupon_code = ( ! empty( $coupon->code ) ) ? $coupon->code : '';
}
$coupon_amount = $this->get_amount( $coupon, true, $order );
if ( 'smart_coupon' === $discount_type && $coupon_amount > 0 ) {
$store_credit_balance .= '<li><strong>' . $coupon_code . '</strong> &mdash; ' . wc_price( $coupon_amount ) . '</li>';
}
}
if ( ! empty( $store_credit_balance ) ) {
/* translators: %s: singular name for store credit */
$balance_left_txt = ! empty( $store_credit_label['singular'] ) ? sprintf( __( '%s Balance ', 'woocommerce-smart-coupons' ), esc_html( ucwords( $store_credit_label['singular'] ) ) ) : __( 'Store Credit / Gift Card Balance', 'woocommerce-smart-coupons' );
echo '<br /><h3>' . esc_html( $balance_left_txt ) . ': </h3>';
echo '<ul>' . wp_kses_post( $store_credit_balance ) . '</ul><br />'; // phpcs:ignore
}
}
}
/**
* Force try to find the coupon's id by code
* in some cases like when coupon is trashed
*
* @param integer $id The coupon's id.
* @param string $code The coupon code.
* @param integer $exclude_id Exclude coupon's id.
* @return integer
*/
public function get_coupon_id_from_code( $id = 0, $code = '', $exclude_id = 0 ) {
if ( empty( $id ) ) {
$backtrace = debug_backtrace( DEBUG_BACKTRACE_IGNORE_ARGS ); // phpcs:ignore
$backtrace_functions = wp_list_pluck( $backtrace, 'function' );
if ( in_array( 'get_total_credit_used_in_order', $backtrace_functions, true ) ) {
$index = array_search( 'get_total_credit_used_in_order', $backtrace_functions, true );
$traced = ( ! empty( $backtrace[ $index ] ) && 'WC_SC_Order_Fields' === $backtrace[ $index ]['class'] && 'get_total_credit_used_in_order' === $backtrace[ $index ]['function'] );
if ( true === $traced ) {
global $wpdb;
$post_id = $wpdb->get_var( // phpcs:ignore
$wpdb->prepare(
"
SELECT ID
FROM $wpdb->posts
WHERE post_title = %s
AND post_type = 'shop_coupon'
ORDER BY ID DESC
",
$code
)
);
if ( ! empty( $post_id ) ) {
return absint( $post_id );
}
}
}
}
return $id;
}
/**
* Function to get total discount applied by store credit.
*
* @param float $discount The original discount.
* @param WC_Order_Item_Coupon $order_item The coupon order item.
* @return float
*/
public function smart_coupon_get_discount( $discount = 0, $order_item = null ) {
if ( ( function_exists( 'wc_tax_enabled' ) && false === wc_tax_enabled() ) || ( get_option( 'woocommerce_calc_taxes' ) === 'no' ) ) {
return $discount;
}
$discount_tax = $this->is_callable( $order_item, 'get_discount_tax' ) ? $order_item->get_discount_tax() : 0;
if ( empty( $discount_tax ) ) {
return $discount;
}
$order_id = ( $this->is_callable( $order_item, 'get_order_id' ) ) ? $order_item->get_order_id() : 0;
if ( $this->is_old_sc_order( $order_id ) ) {
return $discount;
}
if ( is_admin() && $order_item->is_type( 'coupon' ) ) {
if ( $this->is_wc_gte_87() ) {
$coupon_data = json_decode( $order_item->get_meta( 'coupon_info' ), true );
$discount_type = isset( $coupon_data[2] ) && ! empty( $coupon_data[2] ) ? $coupon_data[2] : '';
} else {
$coupon_data = $order_item->get_meta( 'coupon_data' );
$discount_type = isset( $coupon_data['discount_type'] ) && ! empty( $coupon_data['discount_type'] ) ? $coupon_data['discount_type'] : '';
}
if ( ! empty( $discount_type ) && 'smart_coupon' === $discount_type ) {
$backtrace = debug_backtrace( DEBUG_BACKTRACE_IGNORE_ARGS ); // phpcs:ignore
if ( ! empty( $backtrace ) && is_array( $backtrace ) ) {
$args = array();
foreach ( $backtrace as $trace ) {
if ( array_key_exists( 'args', $trace ) ) {
$args = array_merge( $args, array_values( $trace['args'] ) );
}
}
$args = array_filter( array_unique( $args ) );
if ( ! empty( $args ) && is_array( $args ) ) {
foreach ( $args as $path ) {
if ( stripos( $path, 'admin/meta-boxes/views/html-order-items.php' ) !== false ) {
$discount += $discount_tax;
break;
}
}
}
}
}
}
return $discount;
}
}
}
WC_SC_Order_Fields::get_instance();

View File

@@ -0,0 +1,470 @@
<?php
/**
* Coupons via URL
*
* @author StoreApps
* @since 4.7.0
* @version 1.4.0
*
* @package woocommerce-smart-coupons/includes/
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
if ( ! class_exists( 'WC_SC_Print_Coupon' ) ) {
/**
* Class for handling coupons applied via URL
*/
class WC_SC_Print_Coupon {
/**
* Variable to hold instance of WC_SC_Print_Coupon
*
* @var $instance
*/
private static $instance = null;
/**
* HTML template path.
*
* @var string
*/
private $template_html;
/**
* Template path.
*
* @var string
*/
private $template_base;
/**
* Constructor
*/
private function __construct() {
// Use our plugin templates directory as the template base.
$this->template_base = dirname( WC_SC_PLUGIN_FILE ) . '/templates/';
// Email template location.
$print_style = get_option( 'wc_sc_coupon_print_style', 'default' );
$this->template_html = 'print-coupons-' . $print_style . '.php';
add_action( 'init', array( $this, 'may_be_save_terms_page' ) );
add_action( 'admin_notices', array( $this, 'may_be_show_terms_notice' ) );
add_action( 'wp_ajax_wc_sc_terms_notice_action', array( $this, 'terms_notice_action' ) );
add_action( 'wp_loaded', array( $this, 'print_coupon_from_url' ), 20 );
add_filter( 'display_post_states', array( $this, 'display_post_states' ), 10, 2 );
add_filter( 'pre_delete_post', array( $this, 'prevent_deletion_of_terms_page' ), 10, 3 );
add_filter( 'pre_trash_post', array( $this, 'prevent_deletion_of_terms_page' ), 10, 2 );
add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_scripts_and_styles' ), 99 );
}
/**
* Get single instance of WC_SC_Print_Coupon
*
* @return WC_SC_Print_Coupon Singleton object of WC_SC_Print_Coupon
*/
public static function get_instance() {
// Check if instance is already exists.
if ( is_null( self::$instance ) ) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Handle call to functions which is not available in this class
*
* @param string $function_name The function name.
* @param array $arguments Array of arguments passed while calling $function_name.
* @return result of function call
*/
public function __call( $function_name, $arguments = array() ) {
global $woocommerce_smart_coupon;
if ( ! is_callable( array( $woocommerce_smart_coupon, $function_name ) ) ) {
return;
}
if ( ! empty( $arguments ) ) {
return call_user_func_array( array( $woocommerce_smart_coupon, $function_name ), $arguments );
} else {
return call_user_func( array( $woocommerce_smart_coupon, $function_name ) );
}
}
/**
* Function to create and save terms page if already not created
*/
public function may_be_save_terms_page() {
$terms_page_option = $this->get_terms_page_option();
$terms_page_id = get_option( $terms_page_option, false );
// If terms page option is not created yet or may be deleted.
if ( empty( $terms_page_id ) ) {
$terms_page_id = $this->add_terms_page();
$this->update_terms_page_option( $terms_page_id );
}
}
/**
* Function to add create terms page
*/
public function add_terms_page() {
$name = _x( 'wc-sc-coupons-terms', 'Page slug', 'woocommerce-smart-coupons' );
$title = _x( 'Smart Coupons Terms', 'Page title', 'woocommerce-smart-coupons' );
$terms_page_id = $this->create_page( $name, $title, '', 'private' );
return $terms_page_id;
}
/**
* Utility function for creating Smart Coupons' pages
*
* @param string $slug Page slug.
* @param string $page_title Page title.
* @param string $page_content Page content.
* @param string $post_status Page status.
* @param int $post_parent Page parent.
* @return int $page_id Create page id
*/
public function create_page( $slug = '', $page_title = '', $page_content = '', $post_status = 'publish', $post_parent = 0 ) {
$page_id = 0;
if ( empty( $slug ) ) {
return $page_id;
}
$args = array(
'role' => 'administrator',
'orderby' => 'ID',
'order' => 'ASC',
'fields' => 'ID',
);
$admin_user_ids = get_users( $args );
$page_data = array(
'post_author' => current( $admin_user_ids ),
'post_status' => $post_status,
'post_type' => 'page',
'post_name' => $slug,
'post_title' => $page_title,
'post_content' => $page_content,
'post_parent' => $post_parent,
'comment_status' => 'closed',
);
$page_id = wp_insert_post( $page_data );
return $page_id;
}
/**
* Function to get terms page id
*/
public function get_terms_page_id() {
$terms_page_option = $this->get_terms_page_option();
$terms_page_id = get_option( $terms_page_option, false );
return $terms_page_id;
}
/**
* Function to get the option key name for terms page
*/
public function get_terms_page_option() {
// Option key name for Smart Coupons' term page id.
$terms_page_option = 'wc_sc_terms_page_id';
return $terms_page_option;
}
/**
* Function to update terms' page id in the options table
*
* @param int $terms_page_id Page parent.
*/
public function update_terms_page_option( $terms_page_id = 0 ) {
if ( ! empty( $terms_page_id ) && is_numeric( $terms_page_id ) ) {
$terms_page_option = $this->get_terms_page_option();
update_option( $terms_page_option, $terms_page_id, 'no' );
}
}
/**
* Function to show notice for entering terms' page content
*/
public function may_be_show_terms_notice() {
global $pagenow, $post;
$is_coupon_enabled = wc_coupons_enabled();
// Don't show message if coupons are not enabled.
if ( false === $is_coupon_enabled ) {
return;
}
$terms_page_id = $this->get_terms_page_id();
// Return if terms page hasn't been set.
if ( empty( $terms_page_id ) ) {
return;
}
$valid_post_types = array( 'shop_coupon', 'shop_order', 'product' );
$valid_pagenow = array( 'edit.php', 'post.php', 'plugins.php' );
$is_show_terms_notice = get_option( 'wc_sc_is_show_terms_notice', false );
$get_post_type = ( ! empty( $post->ID ) ) ? $this->get_post_type( $post->ID ) : '';
$get_page = ( ! empty( $_GET['page'] ) ) ? wc_clean( wp_unslash( $_GET['page'] ) ) : ''; // phpcs:ignore
$get_tab = ( ! empty( $_GET['tab'] ) ) ? wc_clean( wp_unslash( $_GET['tab'] ) ) : ''; // phpcs:ignore
$is_page = ( in_array( $pagenow, $valid_pagenow, true ) || in_array( $get_post_type, $valid_post_types, true ) || ( 'admin.php' === $pagenow && ( 'wc-smart-coupons' === $get_page || 'wc-smart-coupons' === $get_tab ) ) );
// Terms page notice.
if ( $is_page && 'no' !== $is_show_terms_notice ) {
if ( ! wp_script_is( 'jquery' ) ) {
wp_enqueue_script( 'jquery' );
}
?>
<style type="text/css" media="screen">
#wc_sc_terms_notice .wc_sc_terms_notice_action {
float: right;
padding: 0.5em 0;
text-align: right;
}
</style>
<script type="text/javascript">
jQuery(function(){
jQuery('body').on('click', '#wc_sc_terms_notice a.wc_sc_terms_notice_remove, #wc_sc_terms_notice a.wc_sc_terms_redirect', function(){
let notice_action = jQuery( this ).data('action');
jQuery.ajax({
url: decodeURIComponent( '<?php echo rawurlencode( admin_url( 'admin-ajax.php' ) ); ?>' ),
type: 'post',
dataType: 'json',
data: {
action: 'wc_sc_terms_notice_action',
security: '<?php echo esc_html( wp_create_nonce( 'wc-sc-terms-notice-action' ) ); ?>',
do: notice_action
},
success: function( response ){
if ( response.success != undefined && response.success != '' && response.success == 'yes' ) {
if( response.redirect_url != undefined && response.redirect_url != '' ) {
window.location.href = response.redirect_url;
} else {
jQuery('#wc_sc_terms_notice').fadeOut(500, function(){ jQuery('#wc_sc_terms_notice').remove(); });
}
}
}
});
return false;
});
});
</script>
<div id="wc_sc_terms_notice" class="updated fade">
<div class="wc_sc_terms_notice_action">
<a href="javascript:void(0)" class="wc_sc_terms_notice_remove" data-action="remove"><?php echo esc_html__( 'Never show again', 'woocommerce-smart-coupons' ); ?></a>
</div>
<p>
<?php echo esc_html__( 'Smart Coupons has created a coupon\'s terms page (used during coupon printing) for you. Please edit it as required from', 'woocommerce-smart-coupons' ) . ' <a href="javascript:void(0)" class="wc_sc_terms_redirect" data-action="redirect">' . esc_html__( 'here', 'woocommerce-smart-coupons' ) . '</a>'; ?>
</p>
</div>
<?php
}
}
/**
* Function to handle Smart Coupons terms notice action
*/
public function terms_notice_action() {
check_ajax_referer( 'wc-sc-terms-notice-action', 'security' );
$post_do = ( ! empty( $_POST['do'] ) ) ? wc_clean( wp_unslash( $_POST['do'] ) ) : ''; // phpcs:ignore
if ( empty( $post_do ) ) {
return;
}
$response = array(
'success' => 'no',
);
$option_updated = update_option( 'wc_sc_is_show_terms_notice', 'no', 'no' );
if ( true === $option_updated ) {
$response['success'] = 'yes';
if ( 'redirect' === $post_do ) {
$terms_page_id = $this->get_terms_page_id();
$terms_edit_url = get_edit_post_link( $terms_page_id, 'edit' );
if ( ! empty( $terms_edit_url ) ) {
$response['redirect_url'] = $terms_edit_url;
}
}
}
wp_send_json( $response );
}
/**
* Print coupon codes along with terms' page content
*/
public function print_coupon_from_url() {
global $woocommerce_smart_coupon;
if ( empty( $_SERVER['QUERY_STRING'] ) ) {
return;
}
parse_str( wp_unslash( $_SERVER['QUERY_STRING'] ), $coupon_args ); // phpcs:ignore
$coupon_args = wc_clean( $coupon_args );
if ( ! empty( $coupon_args['print-coupons'] ) && 'yes' === $coupon_args['print-coupons'] && ! empty( $coupon_args['source'] ) && 'wc-smart-coupons' === $coupon_args['source'] && ! empty( $coupon_args['coupon-codes'] ) ) {
$coupon_args['coupon-codes'] = urldecode( $coupon_args['coupon-codes'] );
$coupon_codes = explode( ',', $coupon_args['coupon-codes'] );
$coupon_codes = array_filter( $coupon_codes ); // Remove empty coupon codes if any.
$coupons_data = array();
foreach ( $coupon_codes as $coupon_code ) {
if ( empty( $coupon_code ) ) {
continue;
}
$coupons_data[] = array(
'code' => $coupon_code,
);
}
if ( ! empty( $coupons_data ) ) {
$terms_page_id = $this->get_terms_page_id();
$terms_page_title = '';
$terms_page_content = '';
if ( ! empty( $terms_page_id ) ) {
$terms_page = get_post( $terms_page_id );
if ( is_a( $terms_page, 'WP_Post' ) ) {
$terms_page_title = ( ! empty( $terms_page->post_title ) ) ? $terms_page->post_title : '';
$terms_page_content = ( ! empty( $terms_page->post_content ) ) ? $terms_page->post_content : '';
if ( ! empty( $terms_page_content ) ) {
$terms_page_content = apply_filters( 'the_content', $terms_page_content );
}
}
}
$design = get_option( 'wc_sc_setting_coupon_design', 'basic' );
$background_color = get_option( 'wc_sc_setting_coupon_background_color', '#39cccc' );
$foreground_color = get_option( 'wc_sc_setting_coupon_foreground_color', '#30050b' );
$third_color = get_option( 'wc_sc_setting_coupon_third_color', '#39cccc' );
$show_coupon_description = get_option( 'smart_coupons_show_coupon_description', 'no' );
$valid_designs = $this->get_valid_coupon_designs();
if ( ! in_array( $design, $valid_designs, true ) ) {
$design = 'basic';
}
$coupon_styles = $woocommerce_smart_coupon->get_coupon_styles( $design );
$default_path = $this->template_base;
$template_path = $woocommerce_smart_coupon->get_template_base_dir( $this->template_html );
wc_get_template(
$this->template_html,
array(
'coupon_codes' => $coupons_data,
'terms_page_title' => $terms_page_title,
'terms_page_content' => $terms_page_content,
'background_color' => $background_color,
'foreground_color' => $foreground_color,
'third_color' => $third_color,
'coupon_styles' => $coupon_styles,
'design' => $design,
'show_coupon_description' => $show_coupon_description,
),
$template_path,
$default_path
);
}
exit;
}
}
/**
* Add display state for printable coupon's terms page
*
* @param array $post_states An array of post display states.
* @param WP_Post $post The current post object.
*/
public function display_post_states( $post_states, $post ) {
$terms_page_id = $this->get_terms_page_id();
if ( ! empty( $post->ID ) && ! empty( $terms_page_id ) && absint( $terms_page_id ) === absint( $post->ID ) ) {
if ( isset( $post_states['private'] ) ) {
unset( $post_states['private'] );
}
$post_states['wc_sc_coupons_terms'] = __( 'Used during coupon printing', 'woocommerce-smart-coupons' );
}
return $post_states;
}
/**
* Prevent Deletion Of Terms Page
*
* @param mixed $is_delete Whether to allow deletion or not.
* @param WP_Post $post The post to delete.
* @param boolean $force_delete Whether to permanently delete or not.
* @return mixed
*/
public function prevent_deletion_of_terms_page( $is_delete = null, $post = null, $force_delete = false ) {
$terms_page_id = $this->get_terms_page_id();
if ( ! empty( $post->ID ) && ! empty( $terms_page_id ) && absint( $terms_page_id ) === absint( $post->ID ) ) {
return false;
}
return $is_delete;
}
/**
* Scripts & styles
*/
public function enqueue_scripts_and_styles() {
global $woocommerce_smart_coupon;
if ( empty( $_SERVER['QUERY_STRING'] ) ) {
return;
}
parse_str( wp_unslash( $_SERVER['QUERY_STRING'] ), $coupon_args ); // phpcs:ignore
$coupon_args = wc_clean( $coupon_args );
$design = get_option( 'wc_sc_setting_coupon_design', 'basic' );
if ( ! empty( $coupon_args['print-coupons'] ) && 'yes' === $coupon_args['print-coupons'] && ! empty( $coupon_args['source'] ) && 'wc-smart-coupons' === $coupon_args['source'] && ! empty( $coupon_args['coupon-codes'] ) ) {
if ( 'custom-design' !== $design ) {
if ( ! wp_style_is( 'smart-coupon-designs' ) ) {
wp_enqueue_style( 'smart-coupon-designs' );
}
}
}
}
}
}
WC_SC_Print_Coupon::get_instance();

View File

@@ -0,0 +1,835 @@
<?php
/**
* Handle privacy of customers
*
* @author StoreApps
* @since 3.3.0
* @version 1.5.0
*
* @package woocommerce-smart-coupons/includes/
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
if ( ! class_exists( 'WC_Abstract_Privacy' ) ) {
return;
}
if ( ! class_exists( 'WC_SC_Privacy' ) ) {
/**
* Class for handling privacy for customer & maintaining GDPR compliance of the plugin
*/
class WC_SC_Privacy extends WC_Abstract_Privacy {
/**
* Variable to hold instance of WC_SC_Privacy
*
* @var $instance
*/
private static $instance = null;
/**
* Plugin data
*
* @var $plugin_data
*/
public $plugin_data = array();
/**
* Constructor
*/
public function __construct() {
$this->plugin_data = WC_Smart_Coupons::get_smart_coupons_plugin_data();
parent::__construct( $this->plugin_data['Name'] );
/* translators: Plugin's name */
$this->add_exporter( WC_SC_PLUGIN_DIRNAME . '-coupon-data-exporter', sprintf( __( '%s - Coupon Personal Data Exporter', 'woocommerce-smart-coupons' ), $this->plugin_data['Name'] ), array( $this, 'wc_sc_coupon_data_exporter' ) );
/* translators: Plugin's name */
$this->add_eraser( WC_SC_PLUGIN_DIRNAME . '-coupon-data-eraser', sprintf( __( '%s - Coupon Personal Data Eraser', 'woocommerce-smart-coupons' ), $this->plugin_data['Name'] ), array( $this, 'wc_sc_coupon_data_eraser' ) );
/* translators: Plugin's name */
$this->add_exporter( WC_SC_PLUGIN_DIRNAME . '-order-data-exporter', sprintf( __( '%s - Order Personal Data Exporter', 'woocommerce-smart-coupons' ), $this->plugin_data['Name'] ), array( $this, 'wc_sc_order_data_exporter' ) );
/* translators: Plugin's name */
$this->add_eraser( WC_SC_PLUGIN_DIRNAME . '-order-data-eraser', sprintf( __( '%s - Order Personal Data Eraser', 'woocommerce-smart-coupons' ), $this->plugin_data['Name'] ), array( $this, 'wc_sc_order_data_eraser' ) );
/* translators: Plugin's name */
$this->add_exporter( WC_SC_PLUGIN_DIRNAME . '-user-data-exporter', sprintf( __( '%s - User Personal Data Exporter', 'woocommerce-smart-coupons' ), $this->plugin_data['Name'] ), array( $this, 'wc_sc_user_data_exporter' ) );
/* translators: Plugin's name */
$this->add_eraser( WC_SC_PLUGIN_DIRNAME . '-user-data-eraser', sprintf( __( '%s - User Personal Data Eraser', 'woocommerce-smart-coupons' ), $this->plugin_data['Name'] ), array( $this, 'wc_sc_user_data_eraser' ) );
add_action( 'woocommerce_privacy_before_remove_order_personal_data', array( $this, 'remove_order_personal_data' ) );
add_filter( 'woocommerce_get_settings_account', array( $this, 'account_settings' ) );
}
/**
* Get single instance of WC_SC_Privacy
*
* @return WC_SC_Privacy Singleton object of WC_SC_Privacy
*/
public static function get_instance() {
// Check if instance is already exists.
if ( is_null( self::$instance ) ) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Handle call to functions which is not available in this class
*
* @param string $function_name The function name.
* @param array $arguments Array of arguments passed while calling $function_name.
* @return result of function call
*/
public function __call( $function_name, $arguments = array() ) {
global $woocommerce_smart_coupon;
if ( ! is_callable( array( $woocommerce_smart_coupon, $function_name ) ) ) {
return;
}
if ( ! empty( $arguments ) ) {
return call_user_func_array( array( $woocommerce_smart_coupon, $function_name ), $arguments );
} else {
return call_user_func( array( $woocommerce_smart_coupon, $function_name ) );
}
}
/**
* Gets the message of the privacy to display.
*/
public function get_privacy_message() {
$content = '<h2>' . esc_html__( 'Store Credit/Gift Certificate', 'woocommerce-smart-coupons' ) . '</h2>
<strong>' . esc_html__( 'What we access?', 'woocommerce-smart-coupons' ) . '</strong>
<ul>
<li>' . esc_html__( 'If you are logged in: We access your billing email address saved in your account & billing email address entered during purchase', 'woocommerce-smart-coupons' ) . '</li>
<li>' . esc_html__( 'If you are a visitor: We access your billing email address entered during purchase', 'woocommerce-smart-coupons' ) . '</li>
</ul>
<strong>' . esc_html__( 'What we store & why?', 'woocommerce-smart-coupons' ) . '</strong>
<ul>
<li>' . esc_html__( 'Coupon code generated for you', 'woocommerce-smart-coupons' ) . '</li>
<li>' . esc_html__( 'Coupon code passed via URL', 'woocommerce-smart-coupons' ) . '</li>
<li>' . esc_html__( 'Coupon amount, email & message entered for gift card receiver', 'woocommerce-smart-coupons' ) . '</li>
</ul>
<p>' . esc_html__( 'We store these data so that we can process it for you whenever required.', 'woocommerce-smart-coupons' ) . '</p>';
return $content;
}
/**
* Returns Smart Coupons data based on email.
*
* @param string $email_address The email address.
* @param int $page Pagination.
*
* @return array
*/
protected function get_wc_sc_data( $email_address, $page ) {
global $wpdb;
$wpdb->query( $wpdb->prepare( 'SET SESSION group_concat_max_len=%d', 999999 ) ); // phpcs:ignore
$results = wp_cache_get( 'wc_sc_coupon_data_' . sanitize_key( $email_address ), 'woocommerce_smart_coupons' );
if ( false === $results ) {
$results = $wpdb->get_results( // phpcs:ignore
$wpdb->prepare(
"SELECT p.ID,
p.post_title,
p.post_date,
GROUP_CONCAT( pm.meta_key ORDER BY pm.meta_id SEPARATOR '###' ) AS meta_keys,
GROUP_CONCAT( pm.meta_value ORDER BY pm.meta_id SEPARATOR '###' ) AS meta_values
FROM $wpdb->posts AS p
LEFT JOIN $wpdb->postmeta AS pm
ON ( p.ID = pm.post_id AND p.post_type = %s AND pm.meta_key IN ( %s, %s, %s ) )
WHERE pm.meta_value = %s
OR pm.meta_value LIKE %s
OR pm.meta_value <> ''
GROUP BY p.ID
ORDER BY p.ID",
'shop_coupon',
'discount_type',
'customer_email',
'generated_from_order_id',
'smart_coupon',
'%' . $wpdb->esc_like( '"' . $email_address . '"' ) . '%'
),
ARRAY_A
);
wp_cache_set( 'wc_sc_coupon_data_' . sanitize_key( $email_address ), $results, 'woocommerce_smart_coupons' );
$this->maybe_add_cache_key( 'wc_sc_coupon_data_' . sanitize_key( $email_address ) );
}
$coupon_data = array();
if ( ! empty( $results ) ) {
foreach ( $results as $result ) {
$meta_keys = ( ! empty( $result['meta_keys'] ) ) ? explode( '###', $result['meta_keys'] ) : array();
$meta_values = ( ! empty( $result['meta_values'] ) ) ? explode( '###', $result['meta_values'] ) : array();
if ( count( $meta_keys ) === count( $meta_values ) ) {
$meta_values = array_map( 'maybe_unserialize', $meta_values );
$meta = array_combine( $meta_keys, $meta_values );
if ( empty( $meta['discount_type'] ) || 'smart_coupon' !== $meta['discount_type'] ) {
continue;
}
unset( $meta['discount_type'] );
if ( empty( $meta['customer_email'] ) ) {
continue;
}
$customer_emails = array_unique( $meta['customer_email'] );
$common_email = array_intersect( array( $email_address ), $customer_emails );
if ( empty( $common_email ) ) {
continue;
}
$meta['customer_email'] = current( $common_email );
} else {
continue;
}
if ( empty( $coupon_data[ $result['ID'] ] ) || ! is_array( $coupon_data[ $result['ID'] ] ) ) {
$coupon_data[ $result['ID'] ] = array();
}
$coupon_data[ $result['ID'] ]['coupon_id'] = $result['ID'];
$coupon_data[ $result['ID'] ]['coupon_code'] = $result['post_title'];
$coupon_data[ $result['ID'] ]['created_date'] = $result['post_date'];
if ( ! empty( $meta ) ) {
foreach ( $meta as $key => $value ) {
$coupon_data[ $result['ID'] ][ $key ] = $value;
}
}
}
}
return $coupon_data;
}
/**
* Handle exporting data for Smart Coupons' Coupon data.
*
* @param string $email_address E-mail address to export.
* @param int $page Pagination of data.
*
* @return array
*/
public function wc_sc_coupon_data_exporter( $email_address, $page = 0 ) {
$done = false;
$data_to_export = array();
$coupon_data = $this->get_wc_sc_data( $email_address, (int) $page );
if ( 0 < count( $coupon_data ) ) {
$data = array();
$index = 0;
foreach ( $coupon_data as $coupon_id => $coupon ) {
$data[] = array(
'name' => $coupon['coupon_code'],
'value' => $coupon['customer_email'],
);
}
$data_to_export[] = array(
'group_id' => 'wc_smart_coupons_coupon_data',
'group_label' => __( 'Store Credit/Gift Certificate - Coupon Data', 'woocommerce-smart-coupons' ),
'item_id' => 'wc-smart-coupons-coupon-data-' . sanitize_title( $email_address ),
'data' => $data,
);
$done = 10 > count( $coupon_data );
} else {
$done = true;
}
return array(
'data' => $data_to_export,
'done' => $done,
);
}
/**
* Finds and erases Smart Coupons by email address.
*
* @param string $email_address The user email address.
* @param int $page Pagination.
* @return array An array of personal data in name value pairs
*/
public function wc_sc_coupon_data_eraser( $email_address, $page ) {
$coupon_data = $this->get_wc_sc_data( $email_address, (int) $page );
$done = false;
$items_removed = false;
$items_retained = false;
$messages = array();
foreach ( $coupon_data as $coupon ) {
list( $removed, $retained, $msgs ) = $this->maybe_handle_coupon_data( $coupon );
$items_removed |= $removed;
$items_retained |= $retained;
$messages = array_merge( $messages, $msgs );
}
// Tell core if we have more coupons to work on still.
$done = count( $coupon_data ) < 10;
return array(
'items_removed' => $items_removed,
'items_retained' => $items_retained,
'messages' => $messages,
'done' => $done,
);
}
/**
* Handle eraser of Coupon data
*
* @param array $coupon The coupon data.
* @return array
*/
protected function maybe_handle_coupon_data( $coupon ) {
if ( empty( $coupon ) || ! $this->is_retention_expired( $coupon['created_date'] ) ) {
return array( false, false, array() );
}
if ( $this->is_wc_gte_30() ) {
$_coupon = ( ! empty( $coupon['coupon_id'] ) ) ? new WC_Coupon( $coupon['coupon_id'] ) : null;
if ( $this->is_callable( $_coupon, 'delete_meta_data' ) && $this->is_callable( $_coupon, 'save' ) && $this->is_callable( $_coupon, 'delete' ) ) {
if ( $this->is_callable( $_coupon, 'set_email_restrictions' ) ) {
$_coupon->set_email_restrictions( array() );
}
$_coupon->delete_meta_data( 'customer_email' );
$_coupon->delete_meta_data( 'generated_from_order_id' );
$_coupon->save();
$_coupon->delete( true );
}
} else {
delete_post_meta( $coupon['coupon_id'], 'customer_email' );
delete_post_meta( $coupon['coupon_id'], 'generated_from_order_id' );
wp_delete_post( $coupon['coupon_id'], true );
}
return array( true, false, array( '<strong>' . __( 'Store Credit/Gift Certificate', 'woocommerce-smart-coupons' ) . '</strong> - ' . __( 'Removed Coupon Personal Data', 'woocommerce-smart-coupons' ) ) );
}
/**
* Returns Smart Coupons data based on email.
*
* @param string $email_address The email addres.
* @param int $page Pagination.
*
* @return array
*/
protected function get_wc_sc_user_data( $email_address, $page ) {
$user_data = array();
$user = get_user_by( 'email', $email_address );
if ( ! empty( $user->ID ) ) {
$sc_shortcode_generated_coupons = get_user_meta( $user->ID, '_sc_shortcode_generated_coupons', true );
if ( ! empty( $sc_shortcode_generated_coupons ) ) {
$user_data['shortcode'] = array();
foreach ( $sc_shortcode_generated_coupons as $sc_shortcode_generated_coupons ) {
$user_data['shortcode'][] = implode( ', ', $sc_shortcode_generated_coupons );
}
}
$sc_applied_coupon_from_url = get_user_meta( $user->ID, 'sc_applied_coupon_from_url', true );
if ( ! empty( $sc_applied_coupon_from_url ) ) {
$user_data['url'] = implode( ', ', $sc_applied_coupon_from_url );
}
}
return $user_data;
}
/**
* Handle exporting data for Smart Coupons User data.
*
* @param string $email_address E-mail address to export.
* @param int $page Pagination of data.
*
* @return array
*/
public function wc_sc_user_data_exporter( $email_address, $page = 0 ) {
$done = false;
$data_to_export = array();
$user_data = $this->get_wc_sc_user_data( $email_address, (int) $page );
if ( 0 < count( $user_data ) ) {
$shortcode = array();
$url = array();
$index = 0;
foreach ( $user_data as $key => $value ) {
if ( 'shortcode' === $key ) {
foreach ( $value as $val ) {
$shortcode[] = array(
'name' => __( 'Coupon', 'woocommerce-smart-coupons' ),
'value' => $val,
);
}
$data_to_export[] = array(
'group_id' => 'wc_smart_coupons_coupon_shortcode_data',
'group_label' => __( 'Generated Coupon Data', 'woocommerce-smart-coupons' ),
'item_id' => 'wc-smart-coupons-shorcode-data-' . sanitize_title( $email_address ),
'data' => $shortcode,
);
} elseif ( 'url' === $key ) {
$url[] = array(
'name' => __( 'Coupon' ),
'value' => $value,
);
$data_to_export[] = array(
'group_id' => 'wc_smart_coupons_url_data',
'group_label' => __( 'Coupon passed in URL', 'woocommerce-smart-coupons' ),
'item_id' => 'wc-smart-coupons-url-data-' . sanitize_title( $email_address ),
'data' => $url,
);
}
}
$done = 10 > count( $user_data );
} else {
$done = true;
}
return array(
'data' => $data_to_export,
'done' => $done,
);
}
/**
* Finds and erases Smart Coupons by email address.
*
* @param string $email_address The user email address.
* @param int $page Pagination.
* @return array An array of personal data in name value pairs
*/
public function wc_sc_user_data_eraser( $email_address, $page ) {
$user = get_user_by( 'email', $email_address );
$done = false;
$items_removed = false;
$items_retained = false;
$messages = array();
if ( ! empty( $user->ID ) ) {
$meta_keys = array( '_sc_shortcode_generated_coupons', 'sc_applied_coupon_from_url' );
foreach ( $meta_keys as $meta_key ) {
delete_user_meta( $user->ID, $meta_key );
$removed = true;
$retained = false;
$msgs = array( '<strong>' . __( 'Store Credit/Gift Certificate', 'woocommerce-smart-coupons' ) . '</strong> - ' . __( 'Removed User Personal Data', 'woocommerce-smart-coupons' ) );
$items_removed |= $removed;
$items_retained |= $retained;
$messages = array_merge( $messages, $msgs );
}
}
// Tell core if we have more coupons to work on still.
$done = true;
return array(
'items_removed' => $items_removed,
'items_retained' => $items_retained,
'messages' => $messages,
'done' => $done,
);
}
/**
* Returns Smart Coupons data based on email.
*
* @param string $email_address The email address.
* @param int $page Pagination.
*
* @return array
*/
protected function get_wc_sc_order_data( $email_address, $page ) {
global $wpdb;
$user = get_user_by( 'email', $email_address );
$order_ids = wp_cache_get( 'wc_sc_order_ids_for_' . sanitize_key( $email_address ), 'woocommerce_smart_coupons' );
if ( false === $order_ids ) {
if ( $this->is_hpos() ) {
$order_ids = $wpdb->get_col( // phpcs:ignore
$wpdb->prepare(
"SELECT id
FROM {$wpdb->prefix}wc_orders
WHERE customer_id = %d",
$user->ID
)
);
} else {
$order_ids = $wpdb->get_col( // phpcs:ignore
$wpdb->prepare(
"SELECT pm.post_id
FROM {$wpdb->posts} AS p
LEFT JOIN {$wpdb->postmeta} AS pm
ON ( p.ID = pm.post_id AND p.post_type = %s )
WHERE pm.meta_key = %s
AND pm.meta_value = %d",
'shop_order',
'_customer_user',
$user->ID
)
);
}
wp_cache_set( 'wc_sc_order_ids_for_' . sanitize_key( $email_address ), $order_ids, 'woocommerce_smart_coupons' );
$this->maybe_add_cache_key( 'wc_sc_order_ids_for_' . sanitize_key( $email_address ) );
}
if ( empty( $order_ids ) ) {
return array();
}
$how_many = count( $order_ids );
$placeholders = array_fill( 0, $how_many, '%d' );
if ( $this->is_hpos() ) {
$query = $wpdb->prepare(
"SELECT o.id,
om.meta_value AS sc_coupon_receiver_details
FROM {$wpdb->prefix}wc_orders AS o
LEFT JOIN {$wpdb->prefix}wc_orders_meta AS om
ON ( o.id = om.id AND om.meta_key = %s )",
'sc_coupon_receiver_details'
);
$query .= $wpdb->prepare( 'WHERE o.id IN (' . implode( ',', $placeholders ) . ') ', $order_ids ); // phpcs:ignore
$query .= $wpdb->prepare(
' AND om.meta_value <> %s
GROUP BY o.id
ORDER BY o.id',
''
);
} else {
$query = $wpdb->prepare(
"SELECT p.ID,
pm.meta_value AS sc_coupon_receiver_details
FROM $wpdb->posts AS p
LEFT JOIN $wpdb->postmeta AS pm
ON ( p.ID = pm.post_id AND p.post_type = %s AND pm.meta_key = %s )",
'shop_order',
'sc_coupon_receiver_details'
);
$query .= $wpdb->prepare( 'WHERE p.ID IN (' . implode( ',', $placeholders ) . ') ', $order_ids ); // phpcs:ignore
$query .= $wpdb->prepare(
' AND pm.meta_value <> %s
GROUP BY p.ID
ORDER BY p.ID',
''
);
}
$unique_order_ids = array_unique( $order_ids );
$results = wp_cache_get( 'wc_sc_receiver_detail_in_orders_' . implode( '_', $unique_order_ids ), 'woocommerce_smart_coupons' );
if ( false === $results ) {
$results = $wpdb->get_results( $query, ARRAY_A ); // phpcs:ignore
wp_cache_set( 'wc_sc_receiver_detail_in_orders_' . implode( '_', $unique_order_ids ), $results, 'woocommerce_smart_coupons' );
$this->maybe_add_cache_key( 'wc_sc_receiver_detail_in_orders_' . implode( '_', $unique_order_ids ) );
}
$order_data = array();
if ( ! empty( $results ) ) {
foreach ( $results as $result ) {
$order_data[ $result['ID'] ] = maybe_unserialize( $result['sc_coupon_receiver_details'] );
}
}
return $order_data;
}
/**
* Handle exporting data for Smart Coupons User data.
*
* @param string $email_address E-mail address to export.
* @param int $page Pagination of data.
*
* @return array
*/
public function wc_sc_order_data_exporter( $email_address, $page = 0 ) {
$done = false;
$data_to_export = array();
$order_data = $this->get_wc_sc_order_data( $email_address, (int) $page );
if ( 0 < count( $order_data ) ) {
$index = 0;
foreach ( $order_data as $key => $value ) {
foreach ( $value as $val ) {
$index++;
$data = array();
foreach ( $val as $k => $v ) {
if ( $val['email'] !== $email_address && 'code' === $k ) {
continue;
}
switch ( $k ) {
case 'code':
$name = __( 'Coupon Code', 'woocommerce-smart-coupons' );
break;
case 'amount':
$name = __( 'Coupon Amount', 'woocommerce-smart-coupons' );
break;
case 'email':
$name = __( 'Coupon For', 'woocommerce-smart-coupons' );
break;
case 'message':
$name = __( 'Message', 'woocommerce-smart-coupons' );
break;
}
$data[] = array(
'name' => $name,
'value' => $v,
);
}
$data_to_export[] = array(
'group_id' => 'wc_smart_coupons_order_data_' . $index,
'group_label' => __( 'Store Credit/Gift Certificate - Order Data', 'woocommerce-smart-coupons' ),
'item_id' => 'wc-smart-coupons-order-data-' . $index,
'data' => $data,
);
}
}
$done = 10 > count( $order_data );
} else {
$done = true;
}
return array(
'data' => $data_to_export,
'done' => $done,
);
}
/**
* Finds and erases Smart Coupons by email address.
*
* @param string $email_address The user email address.
* @param int $page Pagination.
* @return array An array of personal data in name value pairs
*/
public function wc_sc_order_data_eraser( $email_address, $page ) {
global $wpdb;
$user = get_user_by( 'email', $email_address );
$orders = wp_cache_get( 'wc_sc_order_by_email_for_' . $user->ID, 'woocommerce_smart_coupons' );
if ( false === $orders ) {
if ( $this->is_hpos() ) {
$orders = $wpdb->get_results( // phpcs:ignore
$wpdb->prepare(
"SELECT date_created_gmt AS created_date,
id
FROM {$wpdb->prefix}wc_orders
WHERE customer_id = %d",
$user->ID
),
ARRAY_A
);
} else {
$orders = $wpdb->get_results( // phpcs:ignore
$wpdb->prepare(
"SELECT p.post_date AS created_date,
pm.post_id
FROM {$wpdb->posts} AS p
LEFT JOIN {$wpdb->postmeta} AS pm
ON ( p.ID = pm.post_id AND p.post_type = %s )
WHERE pm.meta_key = %s
AND pm.meta_value = %d",
'shop_order',
'_customer_user',
$user->ID
),
ARRAY_A
);
}
wp_cache_set( 'wc_sc_order_by_email_for_' . $user->ID, $orders, 'woocommerce_smart_coupons' );
$this->maybe_add_cache_key( 'wc_sc_order_by_email_for_' . $user->ID );
}
$done = false;
$items_removed = false;
$items_retained = false;
$messages = array();
if ( ! empty( $orders ) ) {
foreach ( $orders as $order ) {
list( $removed, $retained, $msgs ) = $this->maybe_handle_order_data( $order );
$items_removed |= $removed;
$items_retained |= $retained;
$messages = array_merge( $messages, $msgs );
}
}
// Tell core if we have more coupons to work on still.
$done = count( $orders ) < 10;
return array(
'items_removed' => $items_removed,
'items_retained' => $items_retained,
'messages' => $messages,
'done' => $done,
);
}
/**
* Handle eraser of Coupon data
*
* @param array $order The order data.
* @return array
*/
protected function maybe_handle_order_data( $order ) {
if ( empty( $order ) || ! $this->is_retention_expired( $order['created_date'] ) ) {
return array( false, false, array() );
}
$order = ( ! empty( $order['post_id'] ) && function_exists( 'wc_get_order' ) ) ? wc_get_order( $order['post_id'] ) : null;
if ( $this->is_callable( $order, 'delete_meta_data' ) && $this->is_callable( $order, 'save' ) ) {
$order->delete_meta_data( 'sc_coupon_receiver_details' );
$order->delete_meta_data( 'gift_receiver_email' );
$order->delete_meta_data( 'gift_receiver_message' );
$order->delete_meta_data( 'gift_sending_timestamp' );
$order->save();
} else {
delete_post_meta( $order['post_id'], 'sc_coupon_receiver_details' );
delete_post_meta( $order['post_id'], 'gift_receiver_email' );
delete_post_meta( $order['post_id'], 'gift_receiver_message' );
delete_post_meta( $order['post_id'], 'gift_sending_timestamp' );
}
return array( true, false, array( '<strong>' . __( 'Store Credit/Gift Certificate', 'woocommerce-smart-coupons' ) . '</strong> - ' . __( 'Removed Order Personal Data', 'woocommerce-smart-coupons' ) ) );
}
/**
* Remove Smart Coupons order personal data
*
* @param WC_Order $order The order object.
*/
public function remove_order_personal_data( $order ) {
$created_date = $order->get_date_created();
$args = array(
'post_id' => $order->get_id(),
'created_date' => $created_date,
);
$result = $this->maybe_handle_order_data( $args );
}
/**
* Checks if create date is passed retention duration.
*
* @param string $created_date The date.
*/
public function is_retention_expired( $created_date ) {
$retention = wc_parse_relative_date_option( get_option( 'woocommerce_smart_coupons_retention' ) );
$is_expired = false;
$time_span = time() - $this->strtotime( $created_date );
if ( empty( $retention ) || empty( $created_date ) || ! isset( $retention['number'] ) || ! isset( $retention['unit'] ) ) {
return false;
}
if ( empty( $retention['number'] ) || ! is_scalar( $retention['number'] ) ) {
return false;
}
if ( ! is_int( $retention['number'] ) ) {
$retention['number'] = (int) $retention['number'];
}
switch ( $retention['unit'] ) {
case 'days':
$retention = $retention['number'] * DAY_IN_SECONDS;
if ( $time_span > $retention ) {
$is_expired = true;
}
break;
case 'weeks':
$retention = $retention['number'] * WEEK_IN_SECONDS;
if ( $time_span > $retention ) {
$is_expired = true;
}
break;
case 'months':
$retention = $retention['number'] * MONTH_IN_SECONDS;
if ( $time_span > $retention ) {
$is_expired = true;
}
break;
case 'years':
$retention = $retention['number'] * YEAR_IN_SECONDS;
if ( $time_span > $retention ) {
$is_expired = true;
}
break;
}
return $is_expired;
}
/**
* Add retention settings to account tab.
*
* @param array $settings Settings.
* @return array $settings Updated
*/
public function account_settings( $settings ) {
$insert_setting = array(
array(
'title' => __( 'Retain Store Credit/Gift Certificate', 'woocommerce-smart-coupons' ),
'desc_tip' => __( 'Store Credit/Gift Certificate that are stored for customers via coupons. If erased, the customer will not be able to use the coupons.', 'woocommerce-smart-coupons' ),
'id' => 'woocommerce_smart_coupons_retention',
'type' => 'relative_date_selector',
'placeholder' => __( 'N/A', 'woocommerce-smart-coupons' ),
'default' => '',
'autoload' => false,
),
);
array_splice( $settings, ( count( $settings ) - 1 ), 0, $insert_setting );
return $settings;
}
}
}
WC_SC_Privacy::get_instance();

View File

@@ -0,0 +1,258 @@
<?php
/**
* Class to show related coupons on a product page.
*
* @package woocommerce-smart-coupons/includes/
* @author StoreApps
* @since 7.12.0
* @version 1.0.0
*/
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
if ( ! class_exists( 'WC_SC_Product_Columns' ) ) {
/**
* Class for handling product columns
*/
class WC_SC_Product_Columns {
/**
* Variable to hold instance of WC_SC_Product_Columns
*
* @var $instance
*/
private static $instance = null;
/**
* Post type.
*
* @var string
*/
protected $list_table_type = 'product';
/**
* Object being shown on the row.
*
* @var object|null
*/
protected $object = null;
/**
* Constructor
*/
private function __construct() {
add_filter( 'manage_' . $this->list_table_type . '_posts_columns', array( $this, 'define_columns' ), 11 );
add_action( 'manage_' . $this->list_table_type . '_posts_custom_column', array( $this, 'render_columns' ), 10, 2 );
}
/**
* Get single instance of WC_SC_Product_Columns
*
* @return WC_SC_Product_Columns Singleton object of WC_SC_Product_Columns
*/
public static function get_instance() {
// Check if instance is already exists.
if ( is_null( self::$instance ) ) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Handle call to functions which is not available in this class
*
* @param string $function_name The function name.
* @param array $arguments Array of arguments passed while calling $function_name.
* @return result of function call
*/
public function __call( $function_name, $arguments = array() ) {
global $woocommerce_smart_coupon;
if ( ! is_callable( array( $woocommerce_smart_coupon, $function_name ) ) ) {
return;
}
if ( ! empty( $arguments ) ) {
return call_user_func_array( array( $woocommerce_smart_coupon, $function_name ), $arguments );
} else {
return call_user_func( array( $woocommerce_smart_coupon, $function_name ) );
}
}
/**
* Define which columns to show on this screen.
*
* @param array $columns Existing columns.
* @return array Updated columns list.
*/
public function define_columns( $columns = array() ) {
if ( ! is_array( $columns ) || empty( $columns ) ) {
$columns = array();
}
$columns['wc_sc_linked_coupons'] = _x( 'Linked coupons', 'Title for coupon column on the products page', 'woocommerce-smart-coupons' );
return $columns;
}
/**
* Pre-fetch any data for the row each column has access to it.
*
* @param int $post_id Post ID being shown.
*/
protected function prepare_row_data( $post_id = 0 ) {
if ( empty( $post_id ) ) {
return;
}
$product_id = 0;
if ( ! empty( $this->object ) ) {
$product = $this->object;
if ( $this->is_wc_gte_30() ) {
$product_id = ( is_object( $product ) && is_callable( array( $product, 'get_id' ) ) ) ? $product->get_id() : 0;
} else {
$product_id = ( ! empty( $product->id ) ) ? $product->id : 0;
}
}
if ( empty( $this->object ) || $product_id !== $post_id ) {
$this->object = wc_get_product( $post_id );
}
}
/**
* Render individual columns.
*
* @param string $column Column ID to render.
* @param integer $post_id Post ID being shown.
*/
public function render_columns( $column = '', $post_id = 0 ) {
if ( empty( $post_id ) ) {
return;
}
if ( empty( $column ) ) {
return;
}
$this->prepare_row_data( $post_id );
switch ( $column ) {
case 'wc_sc_linked_coupons':
$this->render_view_product_coupon_column( $post_id, $this->object );
break;
}
}
/**
* Render linked coupons column on product screen.
*
* @param integer $product_id The Product ID.
* @param WC_Product $product The Product object.
*/
public function render_view_product_coupon_column( $product_id = 0, $product = null ) {
if ( empty( $product_id ) ) {
return;
}
$max_coupons_limit = apply_filters(
'wc_sc_maximum_linked_coupons_limit',
$this->sc_get_option( 'wc_sc_maximum_linked_coupons_limit', 5 ),
array(
'source' => $this,
'product_id' => $product_id,
'product_obj' => $product,
)
);
// Fetch linked coupons from simple & variable products (except variations).
$linked_coupons = ( $this->is_callable( $product, 'get_meta' ) ) ? $product->get_meta( '_coupon_title' ) : $this->get_post_meta( $product_id, '_coupon_title', true );
if ( empty( $linked_coupons ) || ! is_array( $linked_coupons ) ) {
$linked_coupons = array();
} else {
$linked_coupons = array_filter( array_unique( $linked_coupons ) );
}
$linked_coupons_count = count( $linked_coupons );
if ( $linked_coupons_count > $max_coupons_limit ) {
$linked_coupons = array_slice( $linked_coupons, 0, $max_coupons_limit );
} elseif ( $linked_coupons_count < $max_coupons_limit ) { // Try to find more linked coupons if number of found coupons are not matching $max_coupons_limit.
// Try to find linked coupons from the variations.
if ( $product->is_type( 'variable' ) && $product->has_child() ) {
$children = $product->get_children();
foreach ( $children as $variation_id ) {
$variation = wc_get_product( $variation_id );
$variation_linked_coupons = ( $this->is_callable( $variation, 'get_meta' ) ) ? $variation->get_meta( '_coupon_title' ) : $this->get_post_meta( $variation_id, '_coupon_title', true );
if ( empty( $variation_linked_coupons ) || ! is_array( $variation_linked_coupons ) ) {
continue;
}
$linked_coupons = array_merge( $linked_coupons, $variation_linked_coupons );
$linked_coupons = array_filter( array_unique( $linked_coupons ) );
$linked_coupons_count = count( $linked_coupons );
if ( $linked_coupons_count === $max_coupons_limit ) {
break;
} elseif ( $linked_coupons_count > $max_coupons_limit ) {
$linked_coupons = array_slice( $linked_coupons, 0, $max_coupons_limit );
break;
}
}
}
}
if ( empty( $linked_coupons ) || ! is_array( $linked_coupons ) ) {
echo esc_html( '&ndash;' );
return;
}
$linked_coupons = array_values( $linked_coupons );
$coupon_html = array();
foreach ( $linked_coupons as $index => $coupon_code ) {
$coupon_id = wc_get_coupon_id_by_code( $coupon_code );
$coupon_edit_url = add_query_arg(
array(
'post' => $coupon_id,
'action' => 'edit',
),
admin_url( 'post.php' )
);
$coupon_html[] = '<a href="' . esc_url( $coupon_edit_url ) . '" target="_blank" title="' . esc_attr__( 'Open in a new tab', 'woocommerce-smart-coupons' ) . '"><code>' . esc_html( $coupon_code ) . '</code></a>';
}
$linked_coupons_html = apply_filters(
'wc_sc_product_column_linked_coupons_html',
implode( ' , ', $coupon_html ),
array(
'source' => $this,
'product_id' => $product_id,
'product_obj' => $product,
'maximum_linked_coupons_limit' => $max_coupons_limit,
'linked_coupons' => $linked_coupons,
)
);
echo wp_kses_post( $linked_coupons_html );
}
}
}
WC_SC_Product_Columns::get_instance();

View File

@@ -0,0 +1,524 @@
<?php
/**
* Smart Coupons fields in products
*
* @author StoreApps
* @since 3.3.0
* @version 1.4.0
*
* @package woocommerce-smart-coupons/includes/
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
if ( ! class_exists( 'WC_SC_Product_Fields' ) ) {
/**
* Class for handling Smart Coupons' field in products
*/
class WC_SC_Product_Fields {
/**
* Variable to hold instance of WC_SC_Product_Fields
*
* @var $instance
*/
private static $instance = null;
/**
* Constructor
*/
private function __construct() {
add_action( 'woocommerce_product_options_general_product_data', array( $this, 'woocommerce_product_options_coupons' ) );
add_action( 'woocommerce_product_after_variable_attributes', array( $this, 'woocommerce_product_options_coupons_variable' ), 10, 3 );
add_action( 'woocommerce_process_product_meta', array( $this, 'woocommerce_process_product_meta_coupons' ), 10, 2 );
add_action( 'woocommerce_save_product_variation', array( $this, 'woocommerce_process_product_meta_coupons_variable' ), 10, 2 );
}
/**
* Get single instance of WC_SC_Product_Fields
*
* @return WC_SC_Product_Fields Singleton object of WC_SC_Product_Fields
*/
public static function get_instance() {
// Check if instance is already exists.
if ( is_null( self::$instance ) ) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Handle call to functions which is not available in this class
*
* @param string $function_name The function name.
* @param array $arguments Array of arguments passed while calling $function_name.
* @return result of function call
*/
public function __call( $function_name, $arguments = array() ) {
global $woocommerce_smart_coupon;
if ( ! is_callable( array( $woocommerce_smart_coupon, $function_name ) ) ) {
return;
}
if ( ! empty( $arguments ) ) {
return call_user_func_array( array( $woocommerce_smart_coupon, $function_name ), $arguments );
} else {
return call_user_func( array( $woocommerce_smart_coupon, $function_name ) );
}
}
/**
* Function to provide area for entering coupon code
*/
public function woocommerce_product_options_coupons() {
global $post;
$product_type = WC_Product_Factory::get_product_type( $post->ID );
$product = ( ! empty( $post->ID ) && function_exists( 'wc_get_product' ) ) ? wc_get_product( $post->ID ) : null;
$is_callable_product_get_meta = $this->is_callable( $product, 'get_meta' );
$is_send_coupons_on_renewals = ( true === $is_callable_product_get_meta ) ? $product->get_meta( 'send_coupons_on_renewals' ) : get_post_meta( $post->ID, 'send_coupons_on_renewals', true );
echo '<div class="options_group smart-coupons-field">';
$all_discount_types = wc_get_coupon_types();
?>
<p class="form-field smart-coupon-search post_<?php echo esc_attr( $post->ID ); ?>">
<label for="_coupon_title"><?php echo esc_html__( 'Coupons', 'woocommerce-smart-coupons' ); ?></label>
<select class="wc-coupon-search" style="width: 50%;" multiple="multiple" id="_coupon_title_<?php echo esc_attr( $post->ID ); ?>" name="_coupon_title[<?php echo esc_attr( $post->ID ); ?>][]" data-placeholder="<?php echo esc_attr__( 'Search for a coupon&hellip;', 'woocommerce-smart-coupons' ); ?>" data-action="sc_json_search_coupons" data-security="<?php echo esc_attr( wp_create_nonce( 'search-coupons' ) ); ?>" >
<?php
$coupon_titles = ( true === $is_callable_product_get_meta ) ? $product->get_meta( '_coupon_title' ) : get_post_meta( $post->ID, '_coupon_title', true );
if ( ! empty( $coupon_titles ) ) {
foreach ( $coupon_titles as $coupon_title ) {
$coupon = new WC_Coupon( $coupon_title );
$discount_type = $coupon->get_discount_type();
if ( ! empty( $discount_type ) ) {
/* translators: 1. Discount type 2. Discount Type Label */
$discount_type = sprintf( __( ' ( %1$s: %2$s )', 'woocommerce-smart-coupons' ), __( 'Type', 'woocommerce-smart-coupons' ), $all_discount_types[ $discount_type ] );
}
echo '<option value="' . esc_attr( $coupon_title ) . '"' . selected( true, true, false ) . '>' . esc_html( $coupon_title . $discount_type ) . '</option>';
}
}
?>
</select>
<?php
echo wc_help_tip( esc_html__( 'These coupon/s will be given to customers who buy this product. The coupon code will be automatically sent to their email address on purchase.', 'woocommerce-smart-coupons' ) ); // phpcs:ignore
?>
</p>
<p class="form-field send_coupons_on_renewals_field post_<?php echo esc_attr( $post->ID ); ?>" style="display: none;">
<label for="send_coupons_on_renewals"><?php echo esc_html__( 'Send coupons on renewals?', 'woocommerce-smart-coupons' ); ?></label>
<input type="checkbox" class="checkbox" style="" name="send_coupons_on_renewals[<?php echo esc_attr( $post->ID ); ?>]" id="send_coupons_on_renewals_<?php echo esc_attr( $post->ID ); ?>" value="yes" <?php checked( $is_send_coupons_on_renewals, 'yes' ); ?>/>
<?php echo wc_help_tip( esc_html__( 'Check this box to send above coupons on each renewal order.', 'woocommerce-smart-coupons' ) ); // phpcs:ignore ?>
</p>
<?php $this->product_options_admin_js(); ?>
<?php
echo '</div>';
}
/**
* Coupon fields for variation
*
* @param int $loop Position in the loop.
* @param array $variation_data Variation data.
* @param WP_Post $variation Post data.
*/
public function woocommerce_product_options_coupons_variable( $loop = 0, $variation_data = array(), $variation = null ) {
$variation_id = $variation->ID;
$all_discount_types = wc_get_coupon_types();
$product = ( ! empty( $variation_id ) && function_exists( 'wc_get_product' ) ) ? wc_get_product( $variation_id ) : null;
$is_callable_product_get_meta = $this->is_callable( $product, 'get_meta' );
$is_send_coupons_on_renewals = ( true === $is_callable_product_get_meta ) ? $product->get_meta( 'send_coupons_on_renewals' ) : get_post_meta( $variation_id, 'send_coupons_on_renewals', true );
?>
<div class="smart_coupons_product_options_variable smart-coupons-field">
<p class="form-field smart-coupon-search _coupon_title_field post_<?php echo esc_attr( $variation_id ); ?> form-row form-row-full">
<label for="_coupon_title_<?php echo esc_attr( $loop ); ?>"><?php echo esc_html__( 'Coupons', 'woocommerce-smart-coupons' ); ?></label>
<?php echo wc_help_tip( esc_html__( 'These coupon/s will be given to customers who buy this product. The coupon code will be automatically sent to their email address on purchase.', 'woocommerce-smart-coupons' ) ); // phpcs:ignore ?>
<select class="wc-coupon-search" style="width: 100% !important;" multiple="multiple" id="_coupon_title_<?php echo esc_attr( $variation_id ); ?>" name="_coupon_title[<?php echo esc_attr( $variation_id ); ?>][<?php echo esc_attr( $loop ); ?>][]" data-placeholder="<?php echo esc_attr__( 'Search for a coupon&hellip;', 'woocommerce-smart-coupons' ); ?>" data-action="sc_json_search_coupons" data-security="<?php echo esc_attr( wp_create_nonce( 'search-coupons' ) ); ?>" >
<?php
$coupon_titles = ( true === $is_callable_product_get_meta ) ? $product->get_meta( '_coupon_title' ) : get_post_meta( $variation_id, '_coupon_title', true );
if ( ! empty( $coupon_titles ) ) {
foreach ( $coupon_titles as $coupon_title ) {
$coupon = new WC_Coupon( $coupon_title );
$discount_type = $coupon->get_discount_type();
if ( ! empty( $discount_type ) ) {
/* translators: 1. Discount type 2. Discount Type Label */
$discount_type = sprintf( __( ' ( %1$s: %2$s )', 'woocommerce-smart-coupons' ), __( 'Type', 'woocommerce-smart-coupons' ), $all_discount_types[ $discount_type ] );
}
echo '<option value="' . esc_attr( $coupon_title ) . '"' . selected( true, true, false ) . '>' . esc_html( $coupon_title . $discount_type ) . '</option>';
}
}
?>
</select>
</p>
<p class="form-field send_coupons_on_renewals_field post_<?php echo esc_attr( $variation_id ); ?> form-row form-row-full">
<label class="tips" data-tip="<?php echo esc_attr__( 'Check this box to send above coupons on each renewal order.', 'woocommerce-smart-coupons' ); ?>">
<?php echo esc_html__( 'Send coupons on renewals?', 'woocommerce-smart-coupons' ); ?>
<input type="checkbox" class="checkbox" id="send_coupons_on_renewals_<?php echo esc_attr( $variation_id ); ?>" name="send_coupons_on_renewals[<?php echo esc_attr( $variation_id ); ?>][<?php echo esc_attr( $loop ); ?>]" style="margin: 0.1em 0.5em 0.1em 0 !important;" value="yes" <?php checked( $is_send_coupons_on_renewals, 'yes' ); ?>/>
</label>
</p>
<?php $this->product_options_admin_js(); ?>
</div>
<?php
}
/**
* Product options admin JS
*/
public function product_options_admin_js() {
?>
<script type="text/javascript">
jQuery(function(){
var updateSendCouponOnRenewals = function(theElement) {
let element;
if (typeof theElement == "undefined") {
element = jQuery('.smart-coupons-field');
} else {
element = [theElement];
}
jQuery.each(element, function(index, value){
var prodType = jQuery('select#product-type').find('option:selected').val();
<?php if ( $this->is_wc_gte_30() ) { ?>
var associatedCouponCount = jQuery(value).find('.smart-coupon-search span.select2-selection ul.select2-selection__rendered li.select2-selection__choice').length;
<?php } else { ?>
var associatedCouponCount = jQuery(value).find('.smart-coupon-search ul.select2-choices li.select2-search-choice').length;
<?php } ?>
if ( ( prodType == 'subscription' || prodType == 'variable-subscription' || prodType == 'subscription_variation' ) && associatedCouponCount > 0 ) {
jQuery(value).find('p.send_coupons_on_renewals_field').show();
} else {
jQuery(value).find('p.send_coupons_on_renewals_field').hide();
}
});
};
setTimeout(function(){updateSendCouponOnRenewals();}, 100);
jQuery('select#product-type').on('change', function() {
var productType = jQuery(this).find('option:selected').val();
if ( productType == 'simple' || productType == 'variable' || productType == 'subscription' || productType == 'variable-subscription' || productType == 'subscription_variation' ) {
jQuery('.wc-coupon-search').show();
} else {
jQuery('.wc-coupon-search').hide();
}
updateSendCouponOnRenewals();
});
jQuery('.wc-coupon-search').on('change', function(){
let theElement = jQuery(this).parent().parent();
setTimeout( function() {
updateSendCouponOnRenewals(theElement);
}, 10 );
});
if ( typeof getEnhancedSelectFormatString == "undefined" ) {
function getEnhancedSelectFormatString() {
var formatString = {
formatMatches: function( matches ) {
if ( 1 === matches ) {
return smart_coupons_select_params.i18n_matches_1;
}
return smart_coupons_select_params.i18n_matches_n.replace( '%qty%', matches );
},
formatNoMatches: function() {
return smart_coupons_select_params.i18n_no_matches;
},
formatAjaxError: function( jqXHR, textStatus, errorThrown ) {
return smart_coupons_select_params.i18n_ajax_error;
},
formatInputTooShort: function( input, min ) {
var number = min - input.length;
if ( 1 === number ) {
return smart_coupons_select_params.i18n_input_too_short_1
}
return smart_coupons_select_params.i18n_input_too_short_n.replace( '%qty%', number );
},
formatInputTooLong: function( input, max ) {
var number = input.length - max;
if ( 1 === number ) {
return smart_coupons_select_params.i18n_input_too_long_1
}
return smart_coupons_select_params.i18n_input_too_long_n.replace( '%qty%', number );
},
formatSelectionTooBig: function( limit ) {
if ( 1 === limit ) {
return smart_coupons_select_params.i18n_selection_too_long_1;
}
return smart_coupons_select_params.i18n_selection_too_long_n.replace( '%qty%', number );
},
formatLoadMore: function( pageNumber ) {
return smart_coupons_select_params.i18n_load_more;
},
formatSearching: function() {
return smart_coupons_select_params.i18n_searching;
}
};
return formatString;
}
}
<?php if ( $this->is_wc_gte_30() ) { // Ajax product search box. ?>
jQuery( '[class= "wc-coupon-search"]' ).filter( ':not(.enhanced)' ).each( function() {
var select2_args = {
allowClear: jQuery( this ).data( 'allow_clear' ) ? true : false,
placeholder: jQuery( this ).data( 'placeholder' ),
minimumInputLength: jQuery( this ).data( 'minimum_input_length' ) ? jQuery( this ).data( 'minimum_input_length' ) : '3',
escapeMarkup: function( m ) {
return m;
},
ajax: {
url: '<?php echo esc_url( admin_url( 'admin-ajax.php' ) ); ?>',
dataType: 'json',
quietMillis: 250,
data: function( params, page ) {
return {
term: params.term,
action: jQuery( this ).data( 'action' ) || 'sc_json_search_coupons',
security: jQuery( this ).data( 'security' )
};
},
processResults: function( data, page ) {
var terms = [];
if ( data ) {
jQuery.each( data, function( id, text ) {
terms.push( { id: id, text: text } );
});
}
return { results: terms };
},
cache: true
}
};
select2_args = jQuery.extend( select2_args, getEnhancedSelectFormatString() );
jQuery( this ).select2( select2_args );
});
<?php } else { ?>
jQuery( ':input.wc-coupon-search' ).filter( ':not(.enhanced)' ).each( function() {
var select2_args = {
allowClear: jQuery( this ).data( 'allow_clear' ) ? true : false,
placeholder: jQuery( this ).data( 'placeholder' ),
minimumInputLength: jQuery( this ).data( 'minimum_input_length' ) ? jQuery( this ).data( 'minimum_input_length' ) : '3',
escapeMarkup: function( m ) {
return m;
},
ajax: {
url: decodeURIComponent( '<?php echo rawurlencode( admin_url( 'admin-ajax.php' ) ); ?>' ),
dataType: 'json',
quietMillis: 250,
data: function( term, page ) {
return {
term: term,
action: jQuery( this ).data( 'action' ) || 'sc_json_search_coupons',
security: '<?php echo esc_attr( wp_create_nonce( 'search-coupons' ) ); ?>'
};
},
results: function( data, page ) {
var terms = [];
if ( data ) {
jQuery.each( data, function( id, text ) {
terms.push( { id: id, text: text } );
});
}
return { results: terms };
},
cache: true
}
};
if ( jQuery( this ).data( 'multiple' ) === true ) {
select2_args.multiple = true;
select2_args.initSelection = function( element, callback ) {
var data = jQuery.parseJSON( element.attr( 'data-selected' ) );
var selected = [];
jQuery( element.val().split( "," ) ).each( function( i, val ) {
selected.push( { id: val, text: data[ val ] } );
});
return callback( selected );
};
select2_args.formatSelection = function( data ) {
return '<div class="selected-option" data-id="' + data.id + '">' + data.text + '</div>';
};
} else {
select2_args.multiple = false;
select2_args.initSelection = function( element, callback ) {
var data = {id: element.val(), text: element.attr( 'data-selected' )};
return callback( data );
};
}
select2_args = jQuery.extend( select2_args, getEnhancedSelectFormatString() );
jQuery( this ).select2( select2_args ).addClass( 'enhanced' );
});
<?php } ?>
});
</script>
<?php
}
/**
* Function to save coupon code to database
*
* @param int $post_id The post id.
* @param object $post The post object.
*/
public function woocommerce_process_product_meta_coupons( $post_id = 0, $post = null ) {
$post_id = absint( $post_id );
$post_coupon_title = ( ! empty( $_POST['_coupon_title'][ $post_id ] ) ) ? wc_clean( wp_unslash( $_POST['_coupon_title'][ $post_id ] ) ) : ''; // phpcs:ignore
$product = ( ! empty( $post_id ) ) ? wc_get_product( $post_id ) : null;
$is_callable_product_update_meta = $this->is_callable( $product, 'update_meta_data' );
if ( ! empty( $post_coupon_title ) ) {
if ( $this->is_wc_gte_30() ) {
$coupon_titles = $post_coupon_title;
} else {
$coupon_titles = array_filter( array_map( 'trim', explode( ',', $post_coupon_title ) ) );
}
if ( true === $is_callable_product_update_meta ) {
$product->update_meta_data( '_coupon_title', $coupon_titles );
} else {
update_post_meta( $post_id, '_coupon_title', $coupon_titles );
}
} else {
if ( true === $is_callable_product_update_meta ) {
$product->update_meta_data( '_coupon_title', array() );
} else {
update_post_meta( $post_id, '_coupon_title', array() );
}
}
if ( isset( $_POST['send_coupons_on_renewals'][ $post_id ] ) ) { // phpcs:ignore
if ( true === $is_callable_product_update_meta ) {
$product->update_meta_data( 'send_coupons_on_renewals', wc_clean( wp_unslash( $_POST['send_coupons_on_renewals'][ $post_id ] ) ) ); // phpcs:ignore
} else {
update_post_meta( $post_id, 'send_coupons_on_renewals', wc_clean( wp_unslash( $_POST['send_coupons_on_renewals'][ $post_id ] ) ) ); // phpcs:ignore
}
} else {
if ( true === $is_callable_product_update_meta ) {
$product->update_meta_data( 'send_coupons_on_renewals', 'no' );
} else {
update_post_meta( $post_id, 'send_coupons_on_renewals', 'no' );
}
}
if ( $this->is_callable( $product, 'save' ) ) {
$product->save();
}
}
/**
* Function for saving coupon details in product meta
*
* @param integer $variation_id Variation ID.
* @param integer $i Loop ID.
*/
public function woocommerce_process_product_meta_coupons_variable( $variation_id = 0, $i = 0 ) {
if ( empty( $variation_id ) ) {
return;
}
$variation_id = absint( $variation_id );
$variation = ( ! empty( $variation_id ) ) ? wc_get_product( $variation_id ) : null;
$is_callable_variation_update_meta = $this->is_callable( $variation, 'update_meta_data' );
$post_coupon_title = ( ! empty( $_POST['_coupon_title'][ $variation_id ][ $i ] ) ) ? wc_clean( wp_unslash( $_POST['_coupon_title'][ $variation_id ][ $i ] ) ) : ''; // phpcs:ignore
if ( ! empty( $post_coupon_title ) ) {
if ( $this->is_wc_gte_30() ) {
$coupon_titles = $post_coupon_title;
} else {
$coupon_titles = array_filter( array_map( 'trim', explode( ',', $post_coupon_title ) ) );
}
if ( true === $is_callable_variation_update_meta ) {
$variation->update_meta_data( '_coupon_title', $coupon_titles );
} else {
update_post_meta( $variation_id, '_coupon_title', $coupon_titles );
}
} else {
if ( true === $is_callable_variation_update_meta ) {
$variation->update_meta_data( '_coupon_title', array() );
} else {
update_post_meta( $variation_id, '_coupon_title', array() );
}
}
if ( isset( $_POST['send_coupons_on_renewals'][ $variation_id ][ $i ] ) ) { // phpcs:ignore
if ( true === $is_callable_variation_update_meta ) {
$variation->update_meta_data( 'send_coupons_on_renewals', wc_clean( wp_unslash( $_POST['send_coupons_on_renewals'][ $variation_id ][ $i ] ) ) ); // phpcs:ignore
} else {
update_post_meta( $variation_id, 'send_coupons_on_renewals', wc_clean( wp_unslash( $_POST['send_coupons_on_renewals'][ $variation_id ][ $i ] ) ) ); // phpcs:ignore
}
} else {
if ( true === $is_callable_variation_update_meta ) {
$variation->update_meta_data( 'send_coupons_on_renewals', 'no' );
} else {
update_post_meta( $variation_id, 'send_coupons_on_renewals', 'no' );
}
}
if ( $this->is_callable( $variation, 'save' ) ) {
$variation->save();
}
}
}
}
WC_SC_Product_Fields::get_instance();

View File

@@ -0,0 +1,236 @@
<?php
/**
* REST API Coupons controller for Smart Coupons
*
* Handles requests to the wc/sc/v1/coupons endpoint.
*
* @author StoreApps
* @since 4.10.0
* @version 1.2.0
*
* @package woocommerce-smart-coupons/includes/
*/
defined( 'ABSPATH' ) || exit;
if ( ! class_exists( 'WC_SC_REST_Coupons_Controller' ) ) {
/**
* REST API Coupons controller class.
*
* @package Automattic/WooCommerce/RestApi
* @extends WC_REST_CRUD_Controller
*/
class WC_SC_REST_Coupons_Controller extends WC_REST_Coupons_Controller {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wc/v3/sc';
/**
* Route base.
*
* @var string
*/
protected $rest_base = 'coupons';
/**
* Post type.
*
* @var string
*/
protected $post_type = 'shop_coupon';
/**
* Constructor
*/
public function __construct() {
add_filter( "woocommerce_rest_prepare_{$this->post_type}_object", array( $this, 'handle_response_data' ), 99, 3 );
}
/**
* Register the routes for coupons.
*/
public function register_routes() {
register_rest_route(
$this->namespace,
'/' . $this->rest_base,
array(
array(
'methods' => WP_REST_Server::CREATABLE,
'callback' => array( $this, 'create_item' ),
'permission_callback' => array( $this, 'create_item_permissions_check' ),
'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ),
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);
}
/**
* Prepare a single coupon for create or update.
*
* @param WP_REST_Request $request Request object.
* @param bool $creating If is creating a new object.
* @return WP_Error|WC_Data
*/
protected function prepare_object_for_database( $request, $creating = false ) {
global $woocommerce_smart_coupon;
$id = isset( $request['id'] ) ? absint( $request['id'] ) : 0;
$coupon = new WC_Coupon( $id );
$schema = $this->get_item_schema();
$data_keys = array_keys( array_filter( $schema['properties'], array( $this, 'filter_writable_props' ) ) );
$email_restrictions = ( ! empty( $request['email_restrictions'] ) && count( $request['email_restrictions'] ) === 1 ) ? $request['email_restrictions'] : '';
// Validate required POST fields.
if ( $creating ) {
if ( empty( $request['code'] ) ) {
$request['code'] = $woocommerce_smart_coupon->generate_unique_code( $email_restrictions );
} else {
$_coupon = new WC_Coupon( $request['code'] );
$is_auto_generate = ( is_object( $_coupon ) && is_callable( array( $_coupon, 'get_meta' ) ) ) ? $_coupon->get_meta( 'auto_generate_coupon' ) : 'no';
if ( 'yes' === $is_auto_generate ) {
$request['code'] = $woocommerce_smart_coupon->generate_unique_code( $email_restrictions );
foreach ( $data_keys as $key ) {
if ( empty( $request[ $key ] ) ) {
switch ( $key ) {
case 'code':
// do nothing.
break;
case 'meta_data':
$meta_data = ( is_object( $_coupon ) && is_callable( array( $_coupon, 'get_meta_data' ) ) ) ? $_coupon->get_meta_data() : null;
$new_meta_data = array();
if ( ! empty( $meta_data ) ) {
foreach ( $meta_data as $meta ) {
if ( is_object( $meta ) && is_callable( array( $meta, 'get_data' ) ) ) {
$data = $meta->get_data();
if ( isset( $data['id'] ) ) {
unset( $data['id'] );
}
$new_meta_data[] = $data;
}
}
}
$request[ $key ] = $new_meta_data;
break;
case 'description':
$request[ $key ] = ( is_object( $_coupon ) && is_callable( array( $_coupon, 'get_description' ) ) ) ? $_coupon->get_description() : null;
break;
default:
if ( is_callable( array( $_coupon, "get_{$key}" ) ) ) {
$request[ $key ] = $_coupon->{"get_{$key}"}();
}
break;
}
}
}
}
}
}
// Handle all writable props.
foreach ( $data_keys as $key ) {
$value = $request[ $key ];
if ( ! is_null( $value ) ) {
switch ( $key ) {
case 'code':
$coupon_code = wc_format_coupon_code( $value );
$id = $coupon->get_id() ? $coupon->get_id() : 0;
$id_from_code = wc_get_coupon_id_by_code( $coupon_code, $id );
if ( $id_from_code ) {
return new WP_Error( 'woocommerce_rest_coupon_code_already_exists', __( 'The coupon code already exists', 'woocommerce-smart-coupons' ), array( 'status' => 400 ) );
}
$coupon->set_code( $coupon_code );
break;
case 'meta_data':
if ( is_array( $value ) ) {
foreach ( $value as $meta ) {
$coupon->update_meta_data( $meta['key'], $meta['value'], isset( $meta['id'] ) ? $meta['id'] : '' );
}
}
break;
case 'description':
$coupon->set_description( wp_filter_post_kses( $value ) );
break;
default:
if ( is_callable( array( $coupon, "set_{$key}" ) ) ) {
$coupon->{"set_{$key}"}( $value );
}
break;
}
}
}
/**
* Filters an object before it is inserted via the REST API.
*
* The dynamic portion of the hook name, `$this->post_type`,
* refers to the object type slug.
*
* @param WC_Data $coupon Object object.
* @param WP_REST_Request $request Request object.
* @param bool $creating If is creating a new object.
*/
return apply_filters( "woocommerce_rest_pre_insert_{$this->post_type}_object", $coupon, $request, $creating );
}
/**
* Handle REST response data
*
* @param WP_REST_Response|mixed $response The response.
* @param WC_Coupon|mixed $object The object.
* @param WP_REST_Request|mixed $request The request.
* @return WP_REST_Response
*/
public function handle_response_data( $response = null, $object = null, $request = null ) {
global $woocommerce_smart_coupon;
if ( ! empty( $request['sc_is_send_email'] ) && 'yes' === $request['sc_is_send_email'] ) {
$is_send_email = $woocommerce_smart_coupon->is_email_template_enabled();
$email_restrictions = ( ! empty( $request['email_restrictions'] ) ) ? current( $request['email_restrictions'] ) : '';
if ( 'yes' === $is_send_email && ! empty( $email_restrictions ) ) {
$coupon = array(
'code' => ( is_object( $object ) && is_callable( array( $object, 'get_code' ) ) ) ? $object->get_code() : '',
'amount' => ( is_object( $object ) && is_callable( array( $object, 'get_amount' ) ) ) ? $object->get_amount() : 0,
);
$action_args = apply_filters(
'wc_sc_email_coupon_notification_args',
array(
'email' => $email_restrictions,
'coupon' => $coupon,
'discount_type' => ( is_object( $object ) && is_callable( array( $object, 'get_discount_type' ) ) ) ? $object->get_discount_type() : '',
)
);
// Trigger email notification.
do_action( 'wc_sc_email_coupon_notification', $action_args );
}
}
if ( ! empty( $request['sc_is_html'] ) && 'yes' === $request['sc_is_html'] ) {
$data = '';
ob_start();
do_action(
'wc_sc_paint_coupon',
array(
'coupon' => $object,
'with_css' => 'yes',
'with_container' => 'yes',
)
);
$data = ob_get_clean();
$response = rest_ensure_response( $data );
}
return $response;
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,970 @@
<?php
/**
* Smart Coupons Shortcode
*
* @author StoreApps
* @since 3.3.0
* @version 2.9.0
*
* @package woocommerce-smart-coupons/includes/
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
if ( ! class_exists( 'WC_SC_Shortcode' ) ) {
/**
* Class for handling Smart Coupons Shortcode
*/
class WC_SC_Shortcode {
/**
* Variable to hold instance of WC_SC_Shortcode
*
* @var $instance
*/
private static $instance = null;
/**
* Constructor
*/
private function __construct() {
add_action( 'admin_enqueue_scripts', array( $this, 'smart_coupon_shortcode_button_init' ), 20 ); // Use 'admin_enqueue_scripts' instead of 'init' // Credit: Jonathan Desrosiers <jdesrosiers@linchpinagency.com>.
add_action( 'init', array( $this, 'register_smart_coupon_shortcode' ) );
add_action( 'after_wp_tiny_mce', array( $this, 'smart_coupons_after_wp_tiny_mce' ) );
add_filter( 'wc_sc_available_coupon_ids', array( $this, 'filter_coupon_ids' ), 10, 2 );
}
/**
* Get single instance of WC_SC_Shortcode
*
* @return WC_SC_Shortcode Singleton object of WC_SC_Shortcode
*/
public static function get_instance() {
// Check if instance is already exists.
if ( is_null( self::$instance ) ) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Handle call to functions which is not available in this class
*
* @param string $function_name The function name.
* @param array $arguments Array of arguments passed while calling $function_name.
* @return result of function call
*/
public function __call( $function_name, $arguments = array() ) {
global $woocommerce_smart_coupon;
if ( ! is_callable( array( $woocommerce_smart_coupon, $function_name ) ) ) {
return;
}
if ( ! empty( $arguments ) ) {
return call_user_func_array( array( $woocommerce_smart_coupon, $function_name ), $arguments );
} else {
return call_user_func( array( $woocommerce_smart_coupon, $function_name ) );
}
}
/**
* Add Smart Coupons shortcode button in WP editor
*/
public function smart_coupon_shortcode_button_init() {
if ( get_user_option( 'rich_editing' ) === 'true' && ! current_user_can( 'manage_options' ) ) { // Add shortcode button to WP Editor only when the user is from the top level.
return;
}
if ( ! wp_script_is( 'wpdialogs' ) ) {
wp_enqueue_script( 'wpdialogs' );
}
if ( ! wp_style_is( 'wp-jquery-ui-dialog' ) ) {
wp_enqueue_style( 'wp-jquery-ui-dialog' );
}
if ( ! wp_style_is( 'smart-coupon' ) ) {
wp_enqueue_style( 'smart-coupon' );
}
add_filter( 'mce_external_plugins', array( $this, 'smart_coupon_register_tinymce_plugin' ) );
add_filter( 'mce_buttons', array( $this, 'smart_coupon_add_tinymce_button' ) );
}
/**
* Add Smart Coupon short code button in TinyMCE
*
* @param array $plugin_array Existing plugin.
* @return array $plugin array with SMart Coupon shortcode
*/
public function smart_coupon_register_tinymce_plugin( $plugin_array ) {
$plugin_array['sc_shortcode_button'] = plugins_url( 'assets/js/sc-shortcode.js', WC_SC_PLUGIN_FILE );
return $plugin_array;
}
/**
* Add Smart coupon shortcode button in TinyMCE
*
* @param array $buttons Existing button.
* @return array $buttons With Smart Coupons shortcode button
*/
public function smart_coupon_add_tinymce_button( $buttons ) {
$buttons[] = 'sc_shortcode_button';
return $buttons;
}
/**
* Register shortcode for Smart Coupons
*/
public function register_smart_coupon_shortcode() {
add_shortcode( 'smart_coupons', array( $this, 'execute_smart_coupons_shortcode' ) );
add_shortcode( 'wc_sc_available_coupons', array( $this, 'show_available_coupons_shortcode' ) );
}
/**
* Execute Smart Coupons shortcode
*
* @param array $atts Shortcode attributes.
* @return HTML code for coupon to be displayed
*/
public function execute_smart_coupons_shortcode( $atts ) {
if ( is_admin() || wp_doing_ajax() || WC()->is_rest_api_request() ) {
return;
}
ob_start();
global $wpdb, $store_credit_label;
$current_user = wp_get_current_user();
$customer_id = $current_user->ID;
$shortcode = shortcode_atts(
array(
'coupon_code' => '',
'discount_type' => 'smart_coupon',
'coupon_amount' => '',
'individual_use' => 'no',
'product_ids' => '',
'exclude_product_ids' => '',
'usage_limit' => '',
'usage_limit_per_user' => '',
'limit_usage_to_x_items' => '',
'expiry_date' => '',
'apply_before_tax' => 'no',
'free_shipping' => 'no',
'product_categories' => '',
'exclude_product_categories' => '',
'minimum_amount' => '',
'maximum_amount' => '',
'exclude_sale_items' => 'no',
'auto_generate' => 'no',
'coupon_prefix' => '',
'coupon_suffix' => '',
'customer_email' => '',
'coupon_style' => '',
'disable_email' => 'no',
'expiry_days' => '',
'is_email' => 'no',
'is_clickable' => 'no',
),
$atts
);
$_coupon_code = $shortcode['coupon_code'];
$_discount_type = $shortcode['discount_type'];
$_coupon_amount = $shortcode['coupon_amount'];
$_expiry_date = $shortcode['expiry_date'];
$_free_shipping = $shortcode['free_shipping'];
$customer_email = $shortcode['customer_email'];
$coupon_prefix = $shortcode['coupon_prefix'];
$coupon_suffix = $shortcode['coupon_suffix'];
$individual_use = $shortcode['individual_use'];
$minimum_amount = $shortcode['minimum_amount'];
$maximum_amount = $shortcode['maximum_amount'];
$usage_limit = $shortcode['usage_limit'];
$apply_before_tax = $shortcode['apply_before_tax'];
$disable_email = $shortcode['disable_email'];
$expiry_days = $shortcode['expiry_days'];
$is_email = $shortcode['is_email'];
$is_clickable = $shortcode['is_clickable'];
if ( empty( $_coupon_code ) && empty( $_coupon_amount ) ) {
return; // Minimum requirement for shortcode is either $_coupon_code or $_coupon_amount.
}
if ( empty( $customer_email ) ) {
if ( ! ( $current_user instanceof WP_User ) ) {
$current_user = wp_get_current_user();
$customer_email = ( isset( $current_user->user_email ) ) ? $current_user->user_email : '';
} else {
$customer_email = ( ! empty( $current_user->data->user_email ) ) ? $current_user->data->user_email : '';
}
}
if ( ! empty( $_coupon_code ) && ! empty( $customer_email ) ) {
$coupon_exists = wp_cache_get( 'wc_sc_shortcode_coupon_id_' . sanitize_key( $customer_email ), 'woocommerce_smart_coupons' );
if ( false === $coupon_exists ) {
$coupon_exists = $wpdb->get_var( // phpcs:ignore
$wpdb->prepare(
"SELECT ID
FROM {$wpdb->prefix}posts AS posts
LEFT JOIN {$wpdb->prefix}postmeta AS postmeta
ON ( postmeta.post_id = posts.ID )
WHERE posts.post_title = %s
AND posts.post_type = %s
AND posts.post_status = %s
AND postmeta.meta_key = %s
AND postmeta.meta_value LIKE %s",
strtolower( $_coupon_code ),
'shop_coupon',
'publish',
'customer_email',
'%' . $wpdb->esc_like( '"' . $customer_email . '"' ) . '%'
)
);
wp_cache_set( 'wc_sc_shortcode_coupon_id_' . sanitize_key( $customer_email ), $coupon_exists, 'woocommerce_smart_coupons' );
$this->maybe_add_cache_key( 'wc_sc_shortcode_coupon_id_' . sanitize_key( $customer_email ) );
}
} else {
$coupon_exists = null;
}
$is_generate = apply_filters(
'wc_sc_shortcode_always_generate_coupon',
( null === $coupon_exists ),
array(
'source' => $this,
'shortcode_attributes' => $shortcode,
)
);
$_expiry_date = '';
if ( ! wp_style_is( 'smart-coupon' ) ) {
wp_enqueue_style( 'smart-coupon' );
}
$all_discount_types = wc_get_coupon_types();
if ( true === $is_generate ) {
if ( ! empty( $_coupon_code ) ) {
$coupon = new WC_Coupon( $_coupon_code );
$is_callable_coupon_update_meta = $this->is_callable( $coupon, 'update_meta_data' );
if ( $this->is_wc_gte_30() ) {
if ( ! is_object( $coupon ) || ! is_callable( array( $coupon, 'get_id' ) ) ) {
return;
}
$coupon_id = $coupon->get_id();
if ( empty( $coupon_id ) ) {
return;
}
$is_free_shipping = ( $coupon->get_free_shipping() ) ? 'yes' : 'no';
$discount_type = $coupon->get_discount_type();
$expiry_date = $coupon->get_date_expires();
$coupon_code = $coupon->get_code();
} else {
$coupon_id = ( ! empty( $coupon->id ) ) ? $coupon->id : 0;
$is_free_shipping = ( ! empty( $coupon->free_shipping ) ) ? $coupon->free_shipping : '';
$discount_type = ( ! empty( $coupon->discount_type ) ) ? $coupon->discount_type : '';
$expiry_date = ( ! empty( $coupon->expiry_date ) ) ? $coupon->expiry_date : '';
$coupon_code = ( ! empty( $coupon->code ) ) ? $coupon->code : '';
}
$coupon_amount = $this->get_amount( $coupon, true );
$is_callable_coupon_get_meta = $this->is_callable( $coupon, 'get_meta' );
if ( ! empty( $discount_type ) ) {
$is_auto_generate = ( true === $is_callable_coupon_get_meta ) ? $coupon->get_meta( 'auto_generate_coupon' ) : get_post_meta( $coupon_id, 'auto_generate_coupon', true );
$is_disable_email_restriction = ( true === $is_callable_coupon_get_meta ) ? $coupon->get_meta( 'sc_disable_email_restriction' ) : get_post_meta( $coupon_id, 'sc_disable_email_restriction', true );
if ( ( empty( $is_disable_email_restriction ) || 'no' === $is_disable_email_restriction ) && ( empty( $is_auto_generate ) || 'no' === $is_auto_generate ) ) {
$existing_customer_emails = ( $this->is_callable( $coupon, 'get_email_restrictions' ) ) ? $coupon->get_email_restrictions() : get_post_meta( $coupon_id, 'customer_email', true );
if ( empty( $existing_customer_emails ) || ! is_array( $existing_customer_emails ) ) {
$existing_customer_emails = array();
}
$existing_customer_emails[] = $customer_email;
if ( true === $is_callable_coupon_update_meta ) {
$coupon->set_email_restrictions( $existing_customer_emails );
} else {
update_post_meta( $coupon_id, 'customer_email', $existing_customer_emails );
}
}
if ( ! empty( $is_auto_generate ) && 'yes' === $is_auto_generate ) {
if ( 0 === $current_user->ID ) {
if ( 'smart_coupon' === $discount_type ) {
return; // Don't generate & don't show coupon if coupon of the shortcode is store credit & user is guest, otherwise it'll lead to unlimited generation of coupon.
} else {
$new_generated_coupon_code = $coupon_code;
}
} elseif ( true === $is_generate && 'yes' === $is_email ) {
$generated_coupon_details = $this->generate_smart_coupon_action( $customer_email, $coupon_amount, '', $coupon );
$last_element = end( $generated_coupon_details[ $customer_email ] );
$new_generated_coupon_code = $last_element['code'];
} else {
$shortcode_generated_coupon = $this->get_shortcode_generated_coupon( $current_user, $coupon );
if ( empty( $shortcode_generated_coupon ) || ( true === $is_generate && 'yes' === $is_email ) ) {
$generated_coupon_details = $this->generate_smart_coupon_action( $customer_email, $coupon_amount, '', $coupon );
$last_element = end( $generated_coupon_details[ $customer_email ] );
$new_generated_coupon_code = $last_element['code'];
$this->save_shortcode_generated_coupon( $new_generated_coupon_code, $current_user, $coupon );
} else {
$new_generated_coupon_code = $shortcode_generated_coupon;
}
}
} else {
$new_generated_coupon_code = $_coupon_code;
}
}
}
if ( ( ! empty( $_coupon_code ) && empty( $discount_type ) ) || ( empty( $_coupon_code ) ) ) {
if ( empty( $current_user->ID ) && ( 'smart_coupon' === $_discount_type || 'smart_coupon' === $discount_type ) ) {
return; // It'll prevent generation of unlimited coupons for guest.
}
if ( empty( $coupon ) ) {
$coupon = null;
}
$shortcode_generated_coupon = $this->get_shortcode_generated_coupon( $current_user, $coupon );
if ( empty( $shortcode_generated_coupon ) ) {
if ( empty( $_coupon_code ) ) {
$_coupon_code = $this->generate_unique_code( $customer_email );
$_coupon_code = $coupon_prefix . $_coupon_code . $coupon_suffix;
}
$coupon_args = array(
'post_title' => strtolower( $_coupon_code ),
'post_content' => '',
'post_status' => 'publish',
'post_author' => 1,
'post_type' => 'shop_coupon',
'post_parent' => ! empty( $coupon_id ) ? absint( $coupon_id ) : 0,
);
$new_coupon = new WC_Coupon( $coupon_args['post_title'] );
if ( $this->is_wc_greater_than( '6.1.2' ) && $this->is_callable( $new_coupon, 'set_status' ) ) {
$new_coupon->set_status( $coupon_args['post_status'] );
}
$new_coupon_id = $new_coupon->save();
if ( ! empty( $new_coupon_id ) ) {
$coupon_args = array_diff_key( $coupon_args, array_flip( array( 'post_title', 'post_status', 'post_type' ) ) );
$coupon_args['ID'] = $new_coupon_id;
wp_update_post( $coupon_args );
}
$new_coupon_id = absint( $new_coupon_id );
$new_coupon = new WC_Coupon( $new_coupon );
$is_callable_new_coupon_update_meta = $this->is_callable( $new_coupon, 'update_meta_data' );
if ( ! empty( $shortcode['expiry_date'] ) ) {
$timestamp = $this->strtotime( $shortcode['expiry_date'] ) + $this->wc_timezone_offset();
$_expiry_date = gmdate( 'Y-m-d', $timestamp );
} elseif ( ! empty( $expiry_days ) ) {
$timestamp = $this->strtotime( "+$expiry_days days" ) + $this->wc_timezone_offset();
$_expiry_date = gmdate( 'Y-m-d', $timestamp );
}
if ( $this->is_wc_gte_30() ) {
if ( ! empty( $_expiry_date ) ) {
$_expiry_date = $this->strtotime( $_expiry_date ) - $this->wc_timezone_offset();
$_expiry_date = $this->get_date_expires_value( $_expiry_date );
if ( true === $is_callable_new_coupon_update_meta ) {
$new_coupon->set_date_expires( $_expiry_date );
} else {
update_post_meta( $new_coupon_id, 'date_expires', $_expiry_date );
}
}
} else {
if ( true === $is_callable_new_coupon_update_meta ) {
$new_coupon->update_meta_data( 'expiry_date', $_expiry_date );
} else {
update_post_meta( $new_coupon_id, 'expiry_date', $_expiry_date );
}
}
if ( 'smart_coupon' === $_discount_type ) {
$this->update_post_meta( $new_coupon_id, 'wc_sc_original_amount', $_coupon_amount, false );
}
if ( true === $is_callable_new_coupon_update_meta ) {
$new_coupon->set_discount_type( $_discount_type );
$new_coupon->set_amount( $_coupon_amount );
$new_coupon->set_individual_use( $this->wc_string_to_bool( $individual_use ) );
$new_coupon->set_minimum_amount( $minimum_amount );
$new_coupon->set_maximum_amount( $maximum_amount );
$new_coupon->set_usage_limit( $usage_limit );
$new_coupon->set_email_restrictions( array( $customer_email ) );
$new_coupon->update_meta_data( 'apply_before_tax', $apply_before_tax );
$new_coupon->set_free_shipping( $this->wc_string_to_bool( $_free_shipping ) );
$new_coupon->set_product_categories( array() );
$new_coupon->set_excluded_product_categories( array() );
$new_coupon->update_meta_data( 'sc_disable_email_restriction', $disable_email );
} else {
update_post_meta( $new_coupon_id, 'discount_type', $_discount_type );
update_post_meta( $new_coupon_id, 'coupon_amount', $_coupon_amount );
update_post_meta( $new_coupon_id, 'individual_use', $individual_use );
update_post_meta( $new_coupon_id, 'minimum_amount', $minimum_amount );
update_post_meta( $new_coupon_id, 'maximum_amount', $maximum_amount );
update_post_meta( $new_coupon_id, 'usage_limit', $usage_limit );
update_post_meta( $new_coupon_id, 'customer_email', array( $customer_email ) );
update_post_meta( $new_coupon_id, 'apply_before_tax', $apply_before_tax );
update_post_meta( $new_coupon_id, 'free_shipping', $_free_shipping );
update_post_meta( $new_coupon_id, 'product_categories', array() );
update_post_meta( $new_coupon_id, 'exclude_product_categories', array() );
update_post_meta( $new_coupon_id, 'sc_disable_email_restriction', $disable_email );
}
if ( $this->is_callable( $new_coupon, 'save' ) ) {
$new_coupon->save();
}
$new_generated_coupon_code = $_coupon_code;
$this->save_shortcode_generated_coupon( $new_generated_coupon_code, $current_user, $coupon );
} else {
$new_generated_coupon_code = $shortcode_generated_coupon;
}
}
} else {
$new_generated_coupon_code = $_coupon_code;
}
$new_coupon_generated = false;
if ( ! empty( $new_generated_coupon_code ) ) {
$coupon = new WC_Coupon( $new_generated_coupon_code );
$new_coupon_generated = true;
}
if ( $new_coupon_generated ) {
if ( $this->is_wc_gte_30() ) {
if ( ! is_object( $coupon ) || ! is_callable( array( $coupon, 'get_id' ) ) ) {
return;
}
$coupon_id = $coupon->get_id();
if ( empty( $coupon_id ) ) {
return;
}
$is_free_shipping = ( $coupon->get_free_shipping() ) ? 'yes' : 'no';
$discount_type = $coupon->get_discount_type();
$expiry_date = $coupon->get_date_expires();
$coupon_code = $coupon->get_code();
} else {
$coupon_id = ( ! empty( $coupon->id ) ) ? $coupon->id : 0;
$is_free_shipping = ( ! empty( $coupon->free_shipping ) ) ? $coupon->free_shipping : '';
$discount_type = ( ! empty( $coupon->discount_type ) ) ? $coupon->discount_type : '';
$expiry_date = ( ! empty( $coupon->expiry_date ) ) ? $coupon->expiry_date : '';
$coupon_code = ( ! empty( $coupon->code ) ) ? $coupon->code : '';
}
}
$coupon_amount = $this->get_amount( $coupon, true );
$coupon_post = get_post( $coupon_id );
$coupon_data = $this->get_coupon_meta_data( $coupon );
$design = get_option( 'wc_sc_setting_coupon_design', 'basic' );
$background_color = get_option( 'wc_sc_setting_coupon_background_color', '#39cccc' );
$foreground_color = get_option( 'wc_sc_setting_coupon_foreground_color', '#30050b' );
$third_color = get_option( 'wc_sc_setting_coupon_third_color', '#39cccc' );
$show_coupon_description = get_option( 'smart_coupons_show_coupon_description', 'no' );
$valid_designs = $this->get_valid_coupon_designs();
if ( ! in_array( $design, $valid_designs, true ) ) {
$design = 'basic';
}
if ( 'yes' === $is_email ) {
$design = ( 'custom-design' !== $design ) ? 'email-coupon' : $design;
}
?>
<style type="text/css"><?php echo esc_html( wp_strip_all_tags( $this->get_coupon_styles( $design, array( 'is_email' => $is_email ) ), true ) ); // phpcs:ignore ?></style>
<?php
if ( ! in_array( $design, array( 'custom-design', 'email-coupon' ), true ) ) {
?>
<style type="text/css">
:root {
--sc-color1: <?php echo esc_html( $background_color ); ?>;
--sc-color2: <?php echo esc_html( $foreground_color ); ?>;
--sc-color3: <?php echo esc_html( $third_color ); ?>;
}
</style>
<?php
}
?>
<?php
$coupon_type = ( ! empty( $coupon_data['coupon_type'] ) ) ? $coupon_data['coupon_type'] : '';
if ( 'yes' === $is_free_shipping ) {
if ( ! empty( $coupon_type ) ) {
$coupon_type .= __( ' & ', 'woocommerce-smart-coupons' );
}
$coupon_type .= __( 'Free Shipping', 'woocommerce-smart-coupons' );
}
if ( $this->is_wc_gte_30() && $expiry_date instanceof WC_DateTime ) {
$expiry_date = ( is_callable( array( $expiry_date, 'getTimestamp' ) ) ) ? $expiry_date->getTimestamp() : null;
} elseif ( ! is_int( $expiry_date ) ) {
$expiry_date = $this->strtotime( $expiry_date );
}
if ( ! empty( $expiry_date ) && is_int( $expiry_date ) ) {
$expiry_time = ( $this->is_callable( $coupon, 'get_meta' ) ) ? (int) $coupon->get_meta( 'wc_sc_expiry_time' ) : (int) get_post_meta( $coupon_id, 'wc_sc_expiry_time', true );
if ( ! empty( $expiry_time ) ) {
$expiry_date += $expiry_time; // Adding expiry time to expiry date.
}
}
$coupon_description = '';
if ( ! empty( $coupon_post->post_excerpt ) && 'yes' === $show_coupon_description ) {
$coupon_description = $coupon_post->post_excerpt;
}
$is_percent = $this->is_percent_coupon( array( 'coupon_object' => $coupon ) );
$args = array(
'coupon_object' => $coupon,
'coupon_amount' => $coupon_amount,
'amount_symbol' => ( true === $is_percent ) ? '%' : get_woocommerce_currency_symbol(),
'discount_type' => wp_strip_all_tags( $coupon_type ),
'coupon_description' => ( ! empty( $coupon_description ) ) ? $coupon_description : wp_strip_all_tags( $this->generate_coupon_description( array( 'coupon_object' => $coupon ) ) ),
'coupon_code' => $new_generated_coupon_code,
'coupon_expiry' => ( ! empty( $expiry_date ) ) ? $this->get_expiration_format( $expiry_date ) : __( 'Never expires', 'woocommerce-smart-coupons' ),
'thumbnail_src' => $this->get_coupon_design_thumbnail_src(
array(
'design' => $design,
'coupon_object' => $coupon,
)
),
'classes' => '',
'template_id' => $design,
'is_percent' => $is_percent,
);
$coupon_target = '';
$wc_url_coupons_active_urls = get_option( 'wc_url_coupons_active_urls' ); // From plugin WooCommerce URL coupons.
if ( ! empty( $wc_url_coupons_active_urls ) ) {
$coupon_target = ( ! empty( $wc_url_coupons_active_urls[ $coupon_id ]['url'] ) ) ? $wc_url_coupons_active_urls[ $coupon_id ]['url'] : '';
}
if ( ! empty( $coupon_target ) ) {
$coupon_target = home_url( '/' . $coupon_target );
} else {
$coupon_target = home_url( '/?sc-page=shop&coupon-code=' . $coupon_code );
}
$coupon_target = apply_filters( 'sc_coupon_url_in_email', $coupon_target, $coupon );
do_action(
'wc_sc_before_shortcode_smart_coupons_html_start',
array(
'source' => $this,
'shortcode_attributes' => $shortcode,
'coupon_object' => $coupon,
)
);
if ( 'yes' === $is_email ) {
echo '<div style="margin: 10px 0;" title="' . esc_html__( 'Click to visit store. This coupon will be applied automatically.', 'woocommerce-smart-coupons' ) . '">';
}
if ( 'yes' === $is_clickable ) {
echo '<a href="' . esc_url( $coupon_target ) . '" style="color: #444;">';
}
echo '<div id="sc-cc"><div class="sc-coupons-list">';
wc_get_template( 'coupon-design/' . $design . '.php', $args, '', plugin_dir_path( WC_SC_PLUGIN_FILE ) . 'templates/' );
echo '</div></div>';
if ( 'yes' === $is_clickable ) {
echo '</a>';
}
if ( 'yes' === $is_email ) {
echo '</div>';
}
return ob_get_clean();
}
/**
* Show available coupons
*
* @param array $atts Shortcode attributes.
* @return HTML code for coupon to be displayed
*/
public function show_available_coupons_shortcode( $atts ) {
if ( is_admin() || wp_doing_ajax() || WC()->is_rest_api_request() ) {
return;
}
$shortcode = shortcode_atts(
array(
'title' => get_option( 'smart_coupon_cart_page_text' ),
'categories' => '', // expecting comma-separated term ids.
),
$atts,
'wc_sc_available_coupons'
);
// To override shortcode attributes use filter 'shortcode_atts_wc_sc_available_coupons'. For more details refer this https://developer.wordpress.org/reference/functions/shortcode_atts/.
$title = $shortcode['title'];
$shortcode['categories'] = ( ! empty( $shortcode['categories'] ) && is_string( $shortcode['categories'] ) ) ? explode( ',', $shortcode['categories'] ) : array();
if ( ! class_exists( 'WC_SC_Display_Coupons' ) ) {
include_once 'class-wc-sc-display-coupons.php';
}
$wc_sc_display_coupons = WC_SC_Display_Coupons::get_instance();
if ( ! is_object( $wc_sc_display_coupons ) || ! is_callable( array( $wc_sc_display_coupons, 'show_available_coupons' ) ) ) {
return '';
}
ob_start();
$wc_sc_display_coupons->show_available_coupons(
$title,
get_the_title(),
array(
'source' => $this,
'shortcode_atts' => $shortcode,
)
);
$output = ob_get_clean();
$stripped_output = wp_strip_all_tags( $output, true );
if ( ( ! empty( $title ) && $stripped_output === $title ) || empty( $stripped_output ) ) {
$no_output_text = apply_filters(
'wc_sc_shortcode_no_coupon_found_text',
$this->sc_get_option( 'wc_sc_shortcode_no_coupon_found_text', '' ),
array(
'source' => $this,
'shortcode_atts' => $shortcode,
)
);
return ( ! empty( $no_output_text ) ) ? '<p id="wc-sc-shortcode-no-coupon-found-text">' . wp_kses_post( $no_output_text ) . '</p>' : '';
}
return $output;
}
/**
* Function to check whether to generate a new coupon through shortcode for current user
* Don't create if it is already generated.
*
* @param WP_User $current_user The user object.
* @param WC_Coupon $coupon The coupon object.
* @return string $code
*/
public function get_shortcode_generated_coupon( $current_user = null, $coupon = null ) {
$max_in_a_session = get_option( '_sc_max_coupon_generate_in_a_session', 1 );
$max_per_coupon_per_user = get_option( '_sc_max_coupon_per_coupon_per_user', 1 );
if ( $this->is_wc_gte_30() ) {
$coupon_code = ( ! empty( $coupon ) && is_callable( array( $coupon, 'get_code' ) ) ) ? $coupon->get_code() : '';
} else {
$coupon_code = ( ! empty( $coupon->code ) ) ? $coupon->code : '';
}
$code = ( ! empty( $coupon_code ) ) ? $coupon_code : '';
if ( ! empty( $current_user->ID ) ) {
$generated_coupons = get_user_meta( $current_user->ID, '_sc_shortcode_generated_coupons', true );
if ( ! empty( $generated_coupons[ $code ] ) && count( $generated_coupons[ $code ] ) >= $max_per_coupon_per_user ) {
return end( $generated_coupons[ $code ] );
}
}
$session_shortcode_coupons = ( is_object( WC()->session ) && is_callable( array( WC()->session, 'get' ) ) ) ? WC()->session->get( '_sc_session_shortcode_generated_coupons' ) : array();
if ( ! empty( $session_shortcode_coupons[ $code ] ) && count( $session_shortcode_coupons[ $code ] ) >= $max_in_a_session ) {
return end( $session_shortcode_coupons[ $code ] );
}
return false;
}
/**
* Function to save shortcode generated coupon details
*
* @param string $new_code The coupon code.
* @param WP_User $current_user The user object.
* @param WC_Coupon $coupon The coupon object.
*/
public function save_shortcode_generated_coupon( $new_code, $current_user, $coupon ) {
if ( $this->is_wc_gte_30() ) {
$coupon_code = ( ! empty( $coupon ) && is_callable( array( $coupon, 'get_code' ) ) ) ? $coupon->get_code() : '';
} else {
$coupon_code = ( ! empty( $coupon->code ) ) ? $coupon->code : '';
}
$code = ( ! empty( $coupon_code ) ) ? $coupon_code : 0;
$session_shortcode_coupons = ( is_object( WC()->session ) && is_callable( array( WC()->session, 'get' ) ) ) ? WC()->session->get( '_sc_session_shortcode_generated_coupons' ) : array();
if ( empty( $session_shortcode_coupons ) || ! is_array( $session_shortcode_coupons ) ) {
$session_shortcode_coupons = array();
}
if ( empty( $session_shortcode_coupons[ $code ] ) ) {
$session_shortcode_coupons[ $code ] = array();
}
if ( ! in_array( $new_code, $session_shortcode_coupons[ $code ], true ) ) {
$session_shortcode_coupons[ $code ][] = $new_code;
if ( is_object( WC()->session ) && is_callable( array( WC()->session, 'set' ) ) ) {
WC()->session->set( '_sc_session_shortcode_generated_coupons', $session_shortcode_coupons );
}
}
if ( ! empty( $current_user->ID ) ) {
$generated_coupons = get_user_meta( $current_user->ID, '_sc_shortcode_generated_coupons', true );
if ( empty( $generated_coupons ) ) {
$generated_coupons = array();
}
if ( empty( $generated_coupons[ $code ] ) ) {
$generated_coupons[ $code ] = array();
}
if ( ! in_array( $new_code, $generated_coupons[ $code ], true ) ) {
$generated_coupons[ $code ][] = $new_code;
update_user_meta( $current_user->ID, '_sc_shortcode_generated_coupons', $generated_coupons );
}
}
}
/**
* Smart coupon button after TinyMCE
*
* @param mixed $mce_settings The editor settings.
*/
public function smart_coupons_after_wp_tiny_mce( $mce_settings ) {
if ( ! is_admin() || ! current_user_can( 'manage_options' ) ) { // Show shortcode attribute dialog to only top level users.
return;
}
$this->sc_attributes_dialog();
}
/**
* Smart Coupons dialog content for shortcode
*/
public function sc_attributes_dialog() {
?>
<div style="display:none;">
<form id="sc_coupons_attributes" tabindex="-1" style="background-color: #F5F5F5;">
<?php wp_nonce_field( 'internal_coupon_shortcode', '_ajax_coupon_shortcode_nonce', false ); ?>
<script type="text/javascript">
jQuery(function(){
jQuery('input#search-coupon-field').on('keyup',function() {
jQuery('div#search-results ul').empty();
var searchString = jQuery(this).val().trim();
if ( searchString.length == 0 ) {
jQuery('#default-text').html('<?php echo esc_html__( 'No search term specified.', 'woocommerce-smart-coupons' ); ?>');
return true;
}
if ( searchString.length == 1 ) {
jQuery('#default-text').html('<?php echo esc_html__( 'Enter more than one character to search.', 'woocommerce-smart-coupons' ); ?>');
return true;
}
jQuery.ajax({
url: '<?php echo esc_url( admin_url( 'admin-ajax.php' ) ); ?>',
method: 'GET',
afterTypeDelay: 100,
data: {
action : 'smart_coupons_json_search',
security : '<?php echo esc_attr( wp_create_nonce( 'search-coupons' ) ); ?>',
term : searchString
},
dataType: 'json',
success: function( response ) {
if ( response ) {
jQuery('#default-text').html('<?php echo esc_html__( 'Click to select coupon code.', 'woocommerce-smart-coupons' ); ?>');
} else {
jQuery('#default-text').html('<?php echo esc_html__( 'No coupon code found.', 'woocommerce-smart-coupons' ); ?>');
return;
}
jQuery('div#search-results ul').html('');
jQuery.each(response, function (i, val) {
jQuery('div#search-results ul').append('<li class="'+i+'">'+ i +val.substr(val.indexOf('(')-1)+'</li>');
});
}
});
});
jQuery('div#sc_shortcode_cancel a').on('click', function() {
emptyAllFormElement();
jQuery('.ui-dialog-titlebar-close').trigger('click');
});
function emptyAllFormElement() {
jQuery('#search-coupon-field').val('');
jQuery('#default-text').html('<?php echo esc_html__( 'No search term specified.', 'woocommerce-smart-coupons' ); ?>');
jQuery('#search-results ul').empty();
}
jQuery('div#search-results').on('click', 'ul li', function() {
var couponCode = jQuery(this).attr('class');
jQuery('input#search-coupon-field').val(couponCode);
});
jQuery('input#sc_shortcode_submit').on('click', function() {
var couponShortcode = '[smart_coupons';
var couponCode = jQuery('#search-coupon-field').val();
if ( couponCode != undefined && couponCode != '' ) {
couponShortcode += ' coupon_code="'+couponCode.trim()+'"';
}
couponShortcode += ' is_clickable="yes"';
couponShortcode += ']';
tinyMCE.execCommand("mceInsertContent", false, couponShortcode);
emptyAllFormElement();
jQuery('.ui-dialog-titlebar-close').trigger('click');
});
});
</script>
<div id="coupon-selector">
<div id="coupon-option">
<div>
<label><span><?php echo esc_html__( 'Coupon code', 'woocommerce-smart-coupons' ); ?></span><input id="search-coupon-field" type="text" name="search_coupon_code" placeholder="<?php echo esc_attr__( 'Search coupon...', 'woocommerce-smart-coupons' ); ?>"/></label>
</div>
<div id="search-panel">
<div id="search-results">
<div id="default-text"><?php echo esc_html__( 'No search term specified.', 'woocommerce-smart-coupons' ); ?></div>
<ul></ul>
</div>
</div>
</div>
</div>
<div id="sc-cc">
<div class="coupon-preview sc-coupons-list">
<div class="preview-heading">
<?php echo esc_html__( 'Preview', 'woocommerce-smart-coupons' ); ?>
</div>
<?php
$design = get_option( 'wc_sc_setting_coupon_design', 'basic' );
$background_color = get_option( 'wc_sc_setting_coupon_background_color', '#39cccc' );
$foreground_color = get_option( 'wc_sc_setting_coupon_foreground_color', '#30050b' );
$third_color = get_option( 'wc_sc_setting_coupon_third_color', '#39cccc' );
$show_coupon_description = get_option( 'smart_coupons_show_coupon_description', 'no' );
$valid_designs = $this->get_valid_coupon_designs();
if ( ! in_array( $design, $valid_designs, true ) ) {
$design = 'basic';
}
?>
<style type="text/css"><?php echo esc_html( wp_strip_all_tags( $this->get_coupon_styles( $design ), true ) ); // phpcs:ignore ?></style>
<?php
if ( 'custom-design' !== $design ) {
?>
<style type="text/css">
:root {
--sc-color1: <?php echo esc_html( $background_color ); ?>;
--sc-color2: <?php echo esc_html( $foreground_color ); ?>;
--sc-color3: <?php echo esc_html( $third_color ); ?>;
}
</style>
<?php
}
?>
<?php
$args = array(
'coupon_amount' => 'XX',
'amount_symbol' => get_woocommerce_currency_symbol(),
'discount_type' => 'Discount type',
'coupon_description' => 'Description',
'coupon_code' => 'coupon-code',
'coupon_expiry' => 'Expires on xx date',
'thumbnail_src' => '',
'classes' => '',
'template_id' => $design,
'is_percent' => false,
);
wc_get_template( 'coupon-design/' . $design . '.php', $args, '', plugin_dir_path( WC_SC_PLUGIN_FILE ) . 'templates/' );
?>
</div>
</div>
<div class="submitbox">
<div id="sc_shortcode_update">
<input type="button" value="<?php echo esc_attr__( 'Insert Shortcode', 'woocommerce-smart-coupons' ); ?>" class="button-primary" id="sc_shortcode_submit" name="sc_shortcode_submit">
</div>
<div id="sc_shortcode_cancel">
<a class="submitdelete deletion" href="#"><?php echo esc_html__( 'Cancel', 'woocommerce-smart-coupons' ); ?></a>
</div>
</div>
</form>
</div>
<?php
}
/**
* Filter coupon ids.
*
* @param array $coupon_ids The coupon ids to filter.
* @param array $args Additional arguments.
* @return array $coupon_ids
*/
public function filter_coupon_ids( $coupon_ids = array(), $args = array() ) {
if ( ! empty( $args['shortcode_atts']['categories'] ) ) {
$coupon_ids = get_objects_in_term( $args['shortcode_atts']['categories'], 'sc_coupon_category' );
}
return $coupon_ids;
}
}
}
WC_SC_Shortcode::get_instance();

View File

@@ -0,0 +1,623 @@
<?php
/**
* Coupons via URL
*
* @author StoreApps
* @since 3.3.0
* @version 2.2.0
*
* @package woocommerce-smart-coupons/includes/
*/
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
if ( ! class_exists( 'WC_SC_URL_Coupon' ) ) {
/**
* Class for handling coupons applied via URL
*/
class WC_SC_URL_Coupon {
/**
* Variable to hold instance of WC_SC_URL_Coupon
*
* @var $instance
*/
private static $instance = null;
/**
* Variable to hold coupon notices
*
* @var $coupon_notices
*/
private $coupon_notices = array();
/**
* Constructor
*/
private function __construct() {
add_action( 'wp_loaded', array( $this, 'apply_coupon_from_url' ), 19 );
add_action( 'wp_loaded', array( $this, 'apply_coupon_from_session' ), 20 );
add_action( 'wp_loaded', array( $this, 'move_applied_coupon_from_cookies_to_account' ) );
add_action( 'wp_head', array( $this, 'convert_sc_coupon_notices_to_wc_notices' ) );
add_filter( 'the_content', array( $this, 'show_coupon_notices' ) );
}
/**
* Get single instance of WC_SC_URL_Coupon
*
* @return WC_SC_URL_Coupon Singleton object of WC_SC_URL_Coupon
*/
public static function get_instance() {
// Check if instance is already exists.
if ( is_null( self::$instance ) ) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Handle call to functions which is not available in this class
*
* @param string $function_name The function name.
* @param array $arguments Array of arguments passed while calling $function_name.
* @return result of function call
*/
public function __call( $function_name, $arguments = array() ) {
global $woocommerce_smart_coupon;
if ( ! is_callable( array( $woocommerce_smart_coupon, $function_name ) ) ) {
return;
}
if ( ! empty( $arguments ) ) {
return call_user_func_array( array( $woocommerce_smart_coupon, $function_name ), $arguments );
} else {
return call_user_func( array( $woocommerce_smart_coupon, $function_name ) );
}
}
/**
* Apply coupon code if passed in the url.
*/
public function apply_coupon_from_url() {
if ( empty( $_SERVER['QUERY_STRING'] ) ) {
return;
}
parse_str( wp_unslash( $_SERVER['QUERY_STRING'] ), $coupon_args ); // phpcs:ignore
$coupon_args = wc_clean( $coupon_args );
if ( ! is_array( $coupon_args ) || empty( $coupon_args ) ) {
return;
}
if ( empty( $coupon_args['coupon-code'] ) ) {
return;
}
$coupons_data = array();
$coupon_args['coupon-code'] = urldecode( $coupon_args['coupon-code'] );
$coupon_codes = explode( ',', $coupon_args['coupon-code'] );
$coupon_codes = array_filter( $coupon_codes ); // Remove empty coupon codes if any.
$max_url_coupons_limit = apply_filters(
'wc_sc_max_url_coupons_limit',
get_option( 'wc_sc_max_url_coupons_limit', 5 ),
array(
'source' => $this,
'query_args' => $coupon_args,
)
);
if ( is_array( $coupon_codes ) ) {
foreach ( $coupon_codes as $coupon_index => $coupon_code ) {
// Process only first five coupons to avoid GET request parameter limit.
if ( $max_url_coupons_limit === $coupon_index ) {
break;
}
if ( empty( $coupon_code ) ) {
continue;
}
$coupons_data[] = array(
'coupon-code' => $coupon_code,
);
}
}
$cart = ( is_object( WC() ) && isset( WC()->cart ) ) ? WC()->cart : null;
$is_cart_empty = is_a( $cart, 'WC_Cart' ) && is_callable( array( $cart, 'is_empty' ) ) && $cart->is_empty();
if ( true === $is_cart_empty ) {
$is_hold = apply_filters(
'wc_sc_hold_applied_coupons',
true,
array(
'coupons_data' => $coupons_data,
'source' => $this,
)
);
if ( true === $is_hold ) {
$this->hold_applied_coupon( $coupons_data );
}
// Set a session cookie to persist the coupon in case the cart is empty. This code will persist the coupon even if the param sc-page is not supplied.
WC()->session->set_customer_session_cookie( true ); // Thanks to: Devon Godfrey.
} else {
foreach ( $coupons_data as $coupon_data ) {
$coupon_code = $coupon_data['coupon-code'];
$coupon = new WC_Coupon( $coupon_code );
if ( ! WC()->cart->has_discount( $coupon_code ) && $this->is_valid( $coupon ) ) {
WC()->cart->add_discount( trim( $coupon_code ) );
}
}
}
if ( ! empty( $coupon_args['add-to-cart'] ) ) {
add_filter( 'woocommerce_add_to_cart_redirect', array( $this, 'add_to_cart_redirect' ), 20, 2 );
return; // Redirection handed over to WooCommerce.
}
if ( empty( $coupon_args['sc-page'] ) ) {
return;
}
$redirect_url = $this->get_sc_redirect_url( $coupon_args );
wp_safe_redirect( $redirect_url );
exit;
}
/**
* Get Smart Coupons redirect url.
*
* @param array $coupon_args Coupon args.
* @return string
*/
public function get_sc_redirect_url( $coupon_args = array() ) {
$redirect_url = '';
if ( empty( $coupon_args ) || ! is_array( $coupon_args ) ) {
return $redirect_url;
}
if ( in_array( $coupon_args['sc-page'], array( 'shop', 'cart', 'checkout', 'myaccount' ), true ) ) {
$page_id = $this->is_wc_gte_30() ? wc_get_page_id( $coupon_args['sc-page'] ) : woocommerce_get_page_id( $coupon_args['sc-page'] );
$redirect_url = get_permalink( $page_id );
} elseif ( is_string( $coupon_args['sc-page'] ) ) {
if ( is_numeric( $coupon_args['sc-page'] ) && ! is_float( $coupon_args['sc-page'] ) ) {
$page = $coupon_args['sc-page'];
} else {
$page = ( function_exists( 'wpcom_vip_get_page_by_path' ) ) ? wpcom_vip_get_page_by_path( $coupon_args['sc-page'], OBJECT, get_post_types() ) : get_page_by_path( $coupon_args['sc-page'], OBJECT, get_post_types() ); // phpcs:ignore
}
$redirect_url = get_permalink( $page );
} elseif ( is_numeric( $coupon_args['sc-page'] ) && ! is_float( $coupon_args['sc-page'] ) ) {
$redirect_url = get_permalink( $coupon_args['sc-page'] );
}
if ( empty( $redirect_url ) ) {
$redirect_url = home_url();
}
// unset known values in the array to re-build URL params below.
if ( isset( $coupon_args['coupon-code'] ) ) {
unset( $coupon_args['coupon-code'] );
}
if ( isset( $coupon_args['sc-page'] ) ) {
unset( $coupon_args['sc-page'] );
}
if ( isset( $coupon_args['add-to-cart'] ) ) {
unset( $coupon_args['add-to-cart'] );
}
// Not using WP's build_query due to performance.
$additional_url_params = http_build_query( $coupon_args );
if ( ! empty( $additional_url_params ) ) {
$redirect_url .= ( ( false === strpos( $additional_url_params, '?' ) ) ? '?' : '&' ) . $additional_url_params;
}
return $this->get_redirect_url_after_smart_coupons_process( $redirect_url );
}
/**
* WooCommerce handles add to cart redirect.
*
* @param string $url The redirect URL.
* @param WC_Product $product Product.
* @return string
*/
public function add_to_cart_redirect( $url = '', $product = null ) {
remove_filter( 'woocommerce_add_to_cart_redirect', array( $this, 'add_to_cart_redirect' ), 20 );
if ( empty( $_SERVER['QUERY_STRING'] ) ) {
return $url;
}
parse_str( wp_unslash( $_SERVER['QUERY_STRING'] ), $coupon_args ); // phpcs:ignore
$coupon_args = wc_clean( $coupon_args );
$cart = ( is_object( WC() ) && isset( WC()->cart ) ) ? WC()->cart : null;
$is_cart_empty = is_a( $cart, 'WC_Cart' ) && is_callable( array( $cart, 'is_empty' ) ) && $cart->is_empty();
if ( false === $is_cart_empty && ! empty( $coupon_args['coupon-code'] ) ) {
$coupon_args['coupon-code'] = urldecode( $coupon_args['coupon-code'] );
$coupon_codes = explode( ',', $coupon_args['coupon-code'] );
$coupon_codes = array_filter( $coupon_codes ); // Remove empty coupon codes if any.
if ( ! empty( $coupon_codes ) ) {
$max_url_coupons_limit = apply_filters( 'wc_sc_max_url_coupons_limit', 5 );
$coupon_codes = ( ! empty( $max_url_coupons_limit ) ) ? array_slice( $coupon_codes, 0, $max_url_coupons_limit ) : array();
foreach ( $coupon_codes as $coupon_code ) {
$coupon = new WC_Coupon( $coupon_code );
if ( ! WC()->cart->has_discount( $coupon_code ) && $this->is_valid( $coupon ) ) {
WC()->cart->add_discount( trim( $coupon_code ) );
}
}
}
}
if ( ! empty( $coupon_args['sc-page'] ) ) {
return $this->get_sc_redirect_url( $coupon_args );
}
return $url;
}
/**
* Apply coupon code from session, if any.
*/
public function apply_coupon_from_session() {
$cart = ( is_object( WC() ) && isset( WC()->cart ) ) ? WC()->cart : null;
if ( empty( $cart ) || WC()->cart->is_empty() ) {
return;
}
$user_id = get_current_user_id();
if ( 0 === $user_id ) {
$unique_id = ( ! empty( $_COOKIE['sc_applied_coupon_profile_id'] ) ) ? wc_clean( wp_unslash( $_COOKIE['sc_applied_coupon_profile_id'] ) ) : ''; // phpcs:ignore
$applied_coupon_from_url = ( ! empty( $unique_id ) ) ? $this->get_applied_coupons_by_guest_user( $unique_id ) : array();
} else {
$applied_coupon_from_url = get_user_meta( $user_id, 'sc_applied_coupon_from_url', true );
}
if ( empty( $applied_coupon_from_url ) || ! is_array( $applied_coupon_from_url ) ) {
return;
}
foreach ( $applied_coupon_from_url as $index => $coupon_code ) {
$coupon = new WC_Coupon( $coupon_code );
if ( $this->is_valid( $coupon ) && ! WC()->cart->has_discount( $coupon_code ) ) {
WC()->cart->add_discount( trim( $coupon_code ) );
unset( $applied_coupon_from_url[ $index ] );
}
}
if ( 0 === $user_id ) {
$this->set_applied_coupon_for_guest_user( $unique_id, $applied_coupon_from_url );
} else {
update_user_meta( $user_id, 'sc_applied_coupon_from_url', $applied_coupon_from_url );
}
}
/**
* Apply coupon code from session, if any.
*
* @param array $coupons_args The coupon arguments.
*/
public function hold_applied_coupon( $coupons_args = array() ) {
if ( empty( $coupons_args ) ) {
return;
}
$user_id = get_current_user_id();
$saved_status = array();
$saved_status = ( 0 === $user_id ) ? $this->save_applied_coupon_in_cookie( $coupons_args ) : $this->save_applied_coupon_in_account( $coupons_args, $user_id );
if ( empty( $saved_status ) ) {
return;
}
foreach ( $coupons_args as $coupon_args ) {
$coupon_code = $coupon_args['coupon-code'];
$save_status = isset( $saved_status[ $coupon_code ] ) ? $saved_status[ $coupon_code ] : '';
if ( 'saved' === $save_status ) {
/* translators: %s: $coupon_code coupon code */
$notice = sprintf( _x( 'Coupon code "%s" applied successfully. Please add some products to the cart to see the discount.', 'This notice will be shown on the cart or the checkout page if the coupon will be applied successfully.', 'woocommerce-smart-coupons' ), $coupon_code );
wc_add_notice( $notice, 'success' );
} elseif ( 'already_saved' === $save_status ) {
/* translators: %s: $coupon_code coupon code */
$notice = sprintf( _x( 'Coupon code "%s" already applied! Please add some products to the cart to see the discount.', 'This notice will be shown on the cart or the checkout page if the coupon is already applied.', 'woocommerce-smart-coupons' ), $coupon_code );
wc_add_notice( $notice, 'error' );
}
}
}
/**
* Apply coupon code from session, if any.
*
* @param array $coupons_args The coupon arguments.
* @return array $saved_status
*/
public function save_applied_coupon_in_cookie( $coupons_args = array() ) {
// Variable to store whether coupons saved/already saved in cookie.
$saved_status = array();
if ( empty( $coupons_args ) ) {
return $saved_status;
}
if ( empty( $_COOKIE['sc_applied_coupon_profile_id'] ) ) {
$unique_id = $this->generate_unique_id();
} else {
$unique_id = wc_clean( wp_unslash( $_COOKIE['sc_applied_coupon_profile_id'] ) ); // phpcs:ignore
}
$applied_coupons = $this->get_applied_coupons_by_guest_user( $unique_id );
foreach ( $coupons_args as $coupon_args ) {
$coupon_code = isset( $coupon_args['coupon-code'] ) ? $coupon_args['coupon-code'] : '';
if ( is_array( $applied_coupons ) && in_array( $coupon_code, $applied_coupons, true ) ) {
$saved_status[ $coupon_code ] = 'already_saved';
} else {
$applied_coupons[] = $coupon_code;
$saved_status[ $coupon_code ] = 'saved';
}
}
$this->set_applied_coupon_for_guest_user( $unique_id, $applied_coupons );
wc_setcookie( 'sc_applied_coupon_profile_id', $unique_id, $this->get_cookie_life() );
return $saved_status;
}
/**
* Apply coupon code from session, if any.
*
* @param array $coupons_args The coupon arguments.
* @param int $user_id The user id.
* @return array $saved_status
*/
public function save_applied_coupon_in_account( $coupons_args = array(), $user_id = 0 ) {
// Variable to store whether coupons saved/already saved in user meta.
$saved_status = array();
if ( ! empty( $coupons_args ) ) {
$applied_coupons = get_user_meta( $user_id, 'sc_applied_coupon_from_url', true );
if ( empty( $applied_coupons ) ) {
$applied_coupons = array();
}
foreach ( $coupons_args as $coupon_args ) {
$coupon_code = $coupon_args['coupon-code'];
if ( ! in_array( $coupon_code, $applied_coupons, true ) ) {
$applied_coupons[] = $coupon_args['coupon-code'];
$saved_status[ $coupon_code ] = 'saved';
} else {
$saved_status[ $coupon_code ] = 'already_saved';
}
}
update_user_meta( $user_id, 'sc_applied_coupon_from_url', $applied_coupons );
}
return $saved_status;
}
/**
* Apply coupon code from session, if any
*/
public function move_applied_coupon_from_cookies_to_account() {
$user_id = get_current_user_id();
if ( $user_id > 0 && ! empty( $_COOKIE['sc_applied_coupon_profile_id'] ) ) {
$unique_id = wc_clean( wp_unslash( $_COOKIE['sc_applied_coupon_profile_id'] ) ); // phpcs:ignore
$applied_coupons = $this->get_applied_coupons_by_guest_user( $unique_id );
if ( false !== $applied_coupons && is_array( $applied_coupons ) && ! empty( $applied_coupons ) ) {
$saved_coupons = get_user_meta( $user_id, 'sc_applied_coupon_from_url', true );
if ( empty( $saved_coupons ) || ! is_array( $saved_coupons ) ) {
$saved_coupons = array();
}
$saved_coupons = array_merge( $saved_coupons, $applied_coupons );
update_user_meta( $user_id, 'sc_applied_coupon_from_url', $saved_coupons );
wc_setcookie( 'sc_applied_coupon_profile_id', '' );
$this->delete_applied_coupons_of_guest_user( $unique_id );
delete_option( 'sc_applied_coupon_profile_' . $unique_id );
}
}
}
/**
* Function to get redirect URL after processing Smart Coupons params
*
* @param string $url The URL.
* @return string $url
*/
public function get_redirect_url_after_smart_coupons_process( $url = '' ) {
if ( empty( $url ) ) {
return $url;
}
$query_string = ( ! empty( $_SERVER['QUERY_STRING'] ) ) ? wc_clean( wp_unslash( $_SERVER['QUERY_STRING'] ) ) : array(); // phpcs:ignore
parse_str( $query_string, $url_args );
$sc_params = array( 'coupon-code', 'sc-page' );
$url_params = array_diff_key( $url_args, array_flip( $sc_params ) );
if ( empty( $url_params['add-to-cart'] ) ) {
$redirect_url = apply_filters( 'wc_sc_redirect_url_after_smart_coupons_process', add_query_arg( $url_params, $url ), array( 'source' => $this ) );
} else {
$redirect_url = apply_filters( 'wc_sc_redirect_url_after_smart_coupons_process', $url, array( 'source' => $this ) );
}
return $redirect_url;
}
/**
* Function to convert sc coupon notices to wc notices
*/
public function convert_sc_coupon_notices_to_wc_notices() {
$coupon_notices = $this->get_coupon_notices();
// If we have coupon notices to be shown and we are on a woocommerce page then convert them to wc notices.
if ( count( $coupon_notices ) > 0 && ( is_woocommerce() || is_cart() || is_checkout() || is_account_page() ) ) {
foreach ( $coupon_notices as $notice_type => $notices ) {
if ( count( $notices ) > 0 ) {
foreach ( $notices as $notice ) {
wc_add_notice( $notice, $notice_type );
}
}
}
$this->remove_coupon_notices();
}
}
/**
* Function to get sc coupon notices
*/
public function get_coupon_notices() {
return apply_filters( 'wc_sc_coupon_notices', $this->coupon_notices );
}
/**
* Function to remove sc coupon notices
*/
public function remove_coupon_notices() {
$this->coupon_notices = array();
}
/**
* Function to add coupon notices to wp content
*
* @param string $content page content.
* @return string $content page content
*/
public function show_coupon_notices( $content = '' ) {
$coupon_notices = $this->get_coupon_notices();
if ( count( $coupon_notices ) > 0 ) {
// Buffer output.
ob_start();
foreach ( $coupon_notices as $notice_type => $notices ) {
if ( count( $coupon_notices[ $notice_type ] ) > 0 ) {
wc_get_template(
"notices/{$notice_type}.php",
array(
'messages' => $coupon_notices[ $notice_type ],
)
);
}
}
$notices = wc_kses_notice( ob_get_clean() );
$content = $notices . $content;
$this->remove_coupon_notices(); // Empty out notice data.
}
return $content;
}
/**
* Function to get coupon codes by guest user's unique ID.
*
* @param string $unique_id Unique ID for guest user.
*
* @return array.
*/
public function get_applied_coupons_by_guest_user( $unique_id = '' ) {
$key = sprintf( 'sc_applied_coupon_profile_%s', $unique_id );
// Get coupons from `transient`.
$coupons = get_transient( $key );
if ( ! empty( $coupons ) && is_array( $coupons ) ) {
return $coupons;
}
// Get coupon from `wp_option`.
return get_option( $key, array() );
}
/**
* Function to set applied coupons for guest user.
*
* @param string $unique_id Unique id for guest user.
* @param array $coupons Array of coupon codes.
*
* @return bool.
*/
public function set_applied_coupon_for_guest_user( $unique_id = '', $coupons = array() ) {
if ( ! empty( $unique_id ) && is_array( $coupons ) ) {
$key = sprintf( 'sc_applied_coupon_profile_%s', $unique_id );
if ( empty( $coupons ) ) {
return delete_transient( $key );
} else {
return set_transient(
$key,
$coupons,
apply_filters( 'wc_sc_applied_coupon_by_url_expire_time', MONTH_IN_SECONDS )
);
}
}
return false;
}
/**
* Function to delete all applied coupons for a guest user.
*
* @param string $unique_id Unique id for guest user.
*
* @return bool.
*/
public function delete_applied_coupons_of_guest_user( $unique_id = '' ) {
if ( ! empty( $unique_id ) ) {
$key = sprintf( 'sc_applied_coupon_profile_%s', $unique_id );
return delete_transient( $key );
}
return false;
}
}
}
WC_SC_URL_Coupon::get_instance();

View File

@@ -0,0 +1,203 @@
<?php
/**
* Compatibility class for WooCommerce 4.4.0
*
* @category Class
* @package WC-compat
* @author StoreApps
* @version 1.1.0
* @since WooCommerce 4.4.0
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
if ( ! class_exists( 'SA_WC_Compatibility_4_4' ) ) {
/**
* Class to check WooCommerce version is greater than and equal to 4.4.0
*/
class SA_WC_Compatibility_4_4 {
/**
* Function to check if WooCommerce is Greater Than And Equal To 4.4.0
*
* @return boolean
*/
public static function is_wc_gte_44() {
return self::is_wc_greater_than( '4.3.3' );
}
/**
* Function to check if WooCommerce is Greater Than And Equal To 4.3.0
*
* @return boolean
*/
public static function is_wc_gte_43() {
return self::is_wc_greater_than( '4.2.2' );
}
/**
* Function to check if WooCommerce is Greater Than And Equal To 4.2.0
*
* @return boolean
*/
public static function is_wc_gte_42() {
return self::is_wc_greater_than( '4.1.1' );
}
/**
* Function to check if WooCommerce is Greater Than And Equal To 4.1.0
*
* @return boolean
*/
public static function is_wc_gte_41() {
return self::is_wc_greater_than( '4.0.1' );
}
/**
* Function to check if WooCommerce is Greater Than And Equal To 4.0.0
*
* @return boolean
*/
public static function is_wc_gte_40() {
return self::is_wc_greater_than( '3.9.3' );
}
/**
* Function to check if WooCommerce is Greater Than And Equal To 3.9.0
*
* @return boolean
*/
public static function is_wc_gte_39() {
return self::is_wc_greater_than( '3.8.1' );
}
/**
* Function to check if WooCommerce is Greater Than And Equal To 3.8.0
*
* @return boolean
*/
public static function is_wc_gte_38() {
return self::is_wc_greater_than( '3.7.1' );
}
/**
* Function to check if WooCommerce is Greater Than And Equal To 3.7.0
*
* @return boolean
*/
public static function is_wc_gte_37() {
return self::is_wc_greater_than( '3.6.5' );
}
/**
* Function to check if WooCommerce is Greater Than And Equal To 3.6.0
*
* @return boolean
*/
public static function is_wc_gte_36() {
return self::is_wc_greater_than( '3.5.8' );
}
/**
* Function to check if WooCommerce is Greater Than And Equal To 3.5.0
*
* @return boolean
*/
public static function is_wc_gte_35() {
return self::is_wc_greater_than( '3.4.7' );
}
/**
* Function to check if WooCommerce is Greater Than And Equal To 3.3.0
*
* @return boolean
*/
public static function is_wc_gte_34() {
return self::is_wc_greater_than( '3.3.5' );
}
/**
* Function to check if WooCommerce is Greater Than And Equal To 3.3.0
*
* @return boolean
*/
public static function is_wc_gte_33() {
return self::is_wc_greater_than( '3.2.6' );
}
/**
* Function to check if WooCommerce is Greater Than And Equal To 3.2.0
*
* @return boolean
*/
public static function is_wc_gte_32() {
return self::is_wc_greater_than( '3.1.2' );
}
/**
* Function to check if WooCommerce is Greater Than And Equal To 3.1.0
*
* @return boolean
*/
public static function is_wc_gte_31() {
return self::is_wc_greater_than( '3.0.9' );
}
/**
* Function to check if WooCommerce is Greater Than And Equal To 3.0.0
*
* @return boolean
*/
public static function is_wc_gte_30() {
return self::is_wc_greater_than( '2.6.14' );
}
/**
* Function to check if WooCommerce version is greater than and equal to 2.6
*
* @return boolean
*/
public static function is_wc_gte_26() {
return self::is_wc_greater_than( '2.5.5' );
}
/**
* Function to check if WooCommerce version is greater than and equal To 2.5
*
* @return boolean
*/
public static function is_wc_gte_25() {
return self::is_wc_greater_than( '2.4.13' );
}
/**
* Function to get WooCommerce version
*
* @return string version or null.
*/
public static function get_wc_version() {
if ( defined( 'WC_VERSION' ) && WC_VERSION ) {
return WC_VERSION;
}
if ( defined( 'WOOCOMMERCE_VERSION' ) && WOOCOMMERCE_VERSION ) {
return WOOCOMMERCE_VERSION;
}
return null;
}
/**
* Function to compare current version of WooCommerce on site with active version of WooCommerce
*
* @param string $version Version number to compare.
* @return bool
*/
public static function is_wc_greater_than( $version ) {
return version_compare( self::get_wc_version(), $version, '>' );
}
}
}

View File

@@ -0,0 +1,33 @@
<?php
/**
* Compatibility class for WooCommerce 8.7.0
*
* @category Class
* @package WC-compat
* @author StoreApps
* @version 1.1.0
* @since WooCommerce 8.7.0
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
if ( ! class_exists( 'SA_WC_Compatibility_8_7' ) ) {
/**
* Class to check WooCommerce version is greater than and equal to 8.7.0
*/
class SA_WC_Compatibility_8_7 extends SA_WC_Compatibility_4_4 {
/**
* Function to check if WooCommerce version is greater than and equal To 8.7
*
* @return boolean
*/
public static function is_wc_gte_87() {
return self::is_wc_greater_than( '8.6.1' );
}
}
}

View File

@@ -0,0 +1,89 @@
<?php
/**
* Compatibility file for WooCommerce Aelia Currency Switcher
*
* @author StoreApps
* @since 6.1.0
* @version 1.0.1
*
* @package woocommerce-smart-coupons/includes/compat/
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
if ( ! class_exists( 'WC_SC_Aelia_CS_Compatibility' ) ) {
/**
* Class for handling compatibility with WooCommerce Aelia Currency Switcher
*/
class WC_SC_Aelia_CS_Compatibility {
/**
* Variable to hold instance of WC_SC_Aelia_CS_Compatibility
*
* @var $instance
*/
private static $instance = null;
/**
* Constructor
*/
public function __construct() {
add_filter( 'wc_aelia_cs_coupon_types_to_convert', array( $this, 'add_smart_coupon' ) );
}
/**
* Get single instance of WC_SC_Aelia_CS_Compatibility
*
* @return WC_SC_Aelia_CS_Compatibility Singleton object of WC_SC_Aelia_CS_Compatibility
*/
public static function get_instance() {
// Check if instance is already exists.
if ( is_null( self::$instance ) ) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Add discount type 'smart_coupon' in Aelia Currency Switcher's framework
*
* @param array $coupon_types Existing coupon types.
* @return array $coupon_types
*/
public function add_smart_coupon( $coupon_types = array() ) {
if ( empty( $coupon_types ) || ! is_array( $coupon_types ) ) {
return $coupon_types;
}
if ( ! in_array( 'smart_coupon', $coupon_types, true ) ) {
$coupon_types[] = 'smart_coupon';
}
return $coupon_types;
}
/**
* Check & convert price
*
* @param float $price The price need to be converted.
* @param string $to_currency The price will be converted to this currency.
* @param string $from_currency The price will be converted from this currency.
* @return float
*/
public function convert_price( $price = 0, $to_currency = null, $from_currency = null ) {
if ( empty( $from_currency ) ) {
$from_currency = get_option( 'woocommerce_currency' ); // Shop base currency.
}
if ( empty( $to_currency ) ) {
$to_currency = get_woocommerce_currency(); // Active currency.
}
return apply_filters( 'wc_aelia_cs_convert', $price, $from_currency, $to_currency );
}
}
}
WC_SC_Aelia_CS_Compatibility::get_instance();

View File

@@ -0,0 +1,140 @@
<?php
/**
* Compatibility file for Klarna Checkout for WooCommerce
*
* @author StoreApps
* @since 7.1.0
* @version 1.0.0
*
* @package woocommerce-smart-coupons/includes/compat/
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
if ( ! class_exists( 'WC_SC_KCO_Compatibility' ) ) {
/**
* Class for handling compatibility with Klarna Checkout for WooCommerce
*/
class WC_SC_KCO_Compatibility {
/**
* Variable to hold instance of WC_SC_KCO_Compatibility
*
* @var $instance
*/
private static $instance = null;
/**
* Constructor
*/
public function __construct() {
add_action( 'wp_loaded', array( $this, 'hooks_for_compatibility' ) );
}
/**
* Add compatibility related functionality
*/
public function hooks_for_compatibility() {
if ( ! function_exists( 'is_plugin_active' ) ) {
require_once ABSPATH . 'wp-admin/includes/plugin.php';
}
if ( is_plugin_active( 'klarna-checkout-for-woocommerce/klarna-checkout-for-woocommerce.php' ) ) {
if ( ! class_exists( 'WC_SC_Purchase_Credit' ) ) {
include_once '../class-wc-sc-purchase-credit.php';
}
if ( class_exists( 'WC_SC_Purchase_Credit' ) ) {
$wc_sc_purchase_credit = WC_SC_Purchase_Credit::get_instance();
$form_position_hook = apply_filters( 'wc_sc_kco_coupon_receiver_detail_form_position_hook', get_option( 'wc_sc_kco_coupon_receiver_detail_form_position_hook', 'kco_wc_before_snippet' ), array( 'source' => $this ) );
add_action( $form_position_hook, array( $wc_sc_purchase_credit, 'gift_certificate_receiver_detail_form' ) );
}
add_action( 'wp_footer', array( $this, 'enqueue_styles_scripts' ), 99 );
}
}
/**
* Get single instance of WC_SC_KCO_Compatibility
*
* @return WC_SC_KCO_Compatibility Singleton object of WC_SC_KCO_Compatibility
*/
public static function get_instance() {
// Check if instance is already exists.
if ( is_null( self::$instance ) ) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Handle call to functions which is not available in this class
*
* @param string $function_name Function to call.
* @param array $arguments Array of arguments passed while calling $function_name.
* @return mixed Result of function call.
*/
public function __call( $function_name, $arguments = array() ) {
global $woocommerce_smart_coupon;
if ( ! is_callable( array( $woocommerce_smart_coupon, $function_name ) ) ) {
return;
}
if ( ! empty( $arguments ) ) {
return call_user_func_array( array( $woocommerce_smart_coupon, $function_name ), $arguments );
} else {
return call_user_func( array( $woocommerce_smart_coupon, $function_name ) );
}
}
/**
* Enqueue required styles/scripts for store credit frontend form
*/
public function enqueue_styles_scripts() {
// Return if gift certificate form is not shown.
if ( ! did_action( 'wc_sc_gift_certificate_form_shown' ) ) {
return;
}
if ( ! wp_script_is( 'jquery' ) ) {
wp_enqueue_script( 'jquery' );
}
?>
<script type="text/javascript">
jQuery(function(){
if ( typeof wc_sc_ajax_save_coupon_receiver_details_in_session === 'undefined' ) {
function wc_sc_ajax_save_coupon_receiver_details_in_session(){
jQuery.ajax({
url: '<?php echo esc_url( is_callable( array( 'WC_AJAX', 'get_endpoint' ) ) ? WC_AJAX::get_endpoint( 'wc_sc_save_coupon_receiver_details' ) : '/?wc-ajax=wc_sc_save_coupon_receiver_details' ); ?>',
type: 'POST',
dataType: 'json',
data: {
security: '<?php echo esc_html( wp_create_nonce( 'wc-sc-save-coupon-receiver-details' ) ); ?>',
data: jQuery( 'form.checkout' ).serialize()
},
success: function( response ) {
if ( 'yes' !== response.success ) {
console.log('<?php echo esc_html__( 'Failed to update coupon receiver details in session.', 'woocommerce-smart-coupons' ); ?>');
}
}
});
}
}
jQuery('body').on('click', '#klarna-checkout-select-other', wc_sc_ajax_save_coupon_receiver_details_in_session);
});
</script>
<?php
}
}
}
WC_SC_KCO_Compatibility::get_instance();

View File

@@ -0,0 +1,107 @@
<?php
/**
* Compatibility file for WooCommerce Points and Rewards
*
* @author StoreApps
* @since 7.8.0
* @version 1.0.0
*
* @package woocommerce-smart-coupons/includes/compat/
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
if ( ! class_exists( 'WC_SC_PNR_Compatibility' ) ) {
/**
* Class for handling compatibility with WooCommerce Points and Rewards
*/
class WC_SC_PNR_Compatibility {
/**
* Variable to hold instance of WC_SC_PNR_Compatibility
*
* @var $instance
*/
private static $instance = null;
/**
* Constructor
*/
public function __construct() {
add_action( 'wp_loaded', array( $this, 'hooks_for_compatibility' ) );
}
/**
* Add compatibility related functionality
*/
public function hooks_for_compatibility() {
if ( ! function_exists( 'is_plugin_active' ) ) {
require_once ABSPATH . 'wp-admin/includes/plugin.php';
}
if ( is_plugin_active( 'woocommerce-points-and-rewards/woocommerce-points-and-rewards.php' ) ) {
add_action( 'wc_sc_before_auto_apply_coupons', array( $this, 'before_auto_apply_coupons' ) );
}
}
/**
* Get single instance of WC_SC_PNR_Compatibility
*
* @return WC_SC_PNR_Compatibility Singleton object of WC_SC_PNR_Compatibility
*/
public static function get_instance() {
// Check if instance is already exists.
if ( is_null( self::$instance ) ) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Handle call to functions which is not available in this class
*
* @param string $function_name Function to call.
* @param array $arguments Array of arguments passed while calling $function_name.
* @return mixed Result of function call.
*/
public function __call( $function_name, $arguments = array() ) {
global $woocommerce_smart_coupon;
if ( ! is_callable( array( $woocommerce_smart_coupon, $function_name ) ) ) {
return;
}
if ( ! empty( $arguments ) ) {
return call_user_func_array( array( $woocommerce_smart_coupon, $function_name ), $arguments );
} else {
return call_user_func( array( $woocommerce_smart_coupon, $function_name ) );
}
}
/**
* Function to execute before auto apply coupons
*
* @param array $args Additional arguments.
*/
public function before_auto_apply_coupons( $args = array() ) {
if ( empty( $args['current_filter'] ) || 'woocommerce_account_content' !== $args['current_filter'] ) {
return;
}
if ( ! has_filter( 'wc_points_rewards_should_render_earn_points_message', '__return_false' ) ) {
add_filter( 'wc_points_rewards_should_render_earn_points_message', '__return_false', 100 );
}
if ( ! has_filter( 'wc_points_rewards_should_render_redeem_points_message', '__return_false' ) ) {
add_filter( 'wc_points_rewards_should_render_redeem_points_message', '__return_false', 100 );
}
}
}
}
WC_SC_PNR_Compatibility::get_instance();

View File

@@ -0,0 +1,88 @@
<?php
/**
* Compatibility file for Woo Multi Currency by VillaTheme https://wordpress.org/plugins/woo-multi-currency/
*
* @author StoreApps
* @since 4.17.0
* @version 1.0.0
*
* @package woocommerce-smart-coupons/includes/compat/
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
if ( ! class_exists( 'WC_SC_WMC_Compatibility' ) ) {
/**
* Class for handling compatibility with Woo Multi Currency
*/
class WC_SC_WMC_Compatibility {
/**
* Variable to hold instance of WC_SC_WMC_Compatibility
*
* @var $instance
*/
private static $instance = null;
/**
* Constructor
*/
private function __construct() {
add_filter( 'wc_sc_credit_called_price_order', array( $this, 'credit_called_price_order' ), 10, 2 );
add_filter( 'wc_sc_credit_called_price_cart', array( $this, 'credit_called_price_cart' ), 10, 2 );
}
/**
* Get single instance of WC_SC_WMC_Compatibility
*
* @return WC_SC_WMC_Compatibility Singleton object of WC_SC_WMC_Compatibility
*/
public static function get_instance() {
// Check if instance is already exists.
if ( is_null( self::$instance ) ) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Function to modify the value of the credit called
*
* @param mized $price The price.
* @param array $args Additional arguments.
* @return mized
*/
public function credit_called_price_order( $price = 0, $args = array() ) {
if ( function_exists( 'wmc_get_price' ) ) {
$order = ( ! empty( $args['order_obj'] ) ) ? $args['order_obj'] : null;
$currency = ( is_object( $order ) && is_callable( array( $order, 'get_currency' ) ) ) ? $order->get_currency() : false;
$price = wmc_get_price( $price, $currency );
}
return $price;
}
/**
* Function to modify the value of the credit called
*
* @param mized $price The price.
* @param array $args Additional arguments.
* @return mized
*/
public function credit_called_price_cart( $price = 0, $args = array() ) {
if ( function_exists( 'wmc_get_price' ) ) {
$price = wmc_get_price( $price );
}
return $price;
}
}
}
WC_SC_WMC_Compatibility::get_instance();

View File

@@ -0,0 +1,137 @@
<?php
/**
* Compatibility file for WooPayments Currency
*
* @author StoreApps
* @since 8.18.0
* @version 1.0.0
* @package woocommerce-smart-coupons/includes/compat/
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
use WCPay\MultiCurrency\MultiCurrency;
if ( ! class_exists( 'WC_SC_WooPayments_Compatibility' ) ) {
/**
* Class for handling compatibility with WooPayments
*/
class WC_SC_WooPayments_Compatibility {
/**
* Variable to hold instance of WC_SC_WooPayments_Compatibility
*
* @var $instance
*/
private static $instance = null;
/**
* Constructor
*/
public function __construct() {
add_filter( 'wcpay_multi_currency_should_convert_product_price', array( $this, 'should_convert_product_price' ), 10, 2 );
}
/**
* Get single instance of WC_SC_WooPayments_Compatibility
*
* @return WC_SC_WooPayments_Compatibility Singleton object of WC_SC_WooPayments_Compatibility
*/
public static function get_instance() {
// Check if instance is already exists.
if ( is_null( self::$instance ) ) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Handle call to functions which is not available in this class
*
* @param string $function_name Function to call.
* @param array $arguments Array of arguments passed while calling $function_name.
* @return mixed Result of function call.
*/
public function __call( $function_name, $arguments = array() ) {
global $woocommerce_smart_coupon;
if ( ! is_callable( array( $woocommerce_smart_coupon, $function_name ) ) ) {
return;
}
if ( ! empty( $arguments ) ) {
return call_user_func_array( array( $woocommerce_smart_coupon, $function_name ), $arguments );
} else {
return call_user_func( array( $woocommerce_smart_coupon, $function_name ) );
}
}
/**
* Check & convert price
*
* @param float $price The price need to be converted.
* @param string $to_currency The price will be converted to this currency.
* @param string $from_currency The price will be converted from this currency.
* @return float
*/
public function convert_price( $price = 0, $to_currency = null, $from_currency = null ) {
$multicurency = MultiCurrency::instance();
if ( is_callable( array( $multicurency, 'get_raw_conversion' ) ) ) {
if ( ! is_float( $price ) ) {
$price = (float) $price;
}
if ( $from_currency !== $to_currency ) {
$price = $multicurency->get_raw_conversion( $price, $to_currency, $from_currency );
if ( ! empty( $price ) && is_float( $price ) ) {
$price = wc_round_tax_total( $price, 4 );
}
}
}
return $price;
}
/**
* Check if product price should be convert or not
*
* @param bool $should_convert should convert or not.
* @param object $product instance of the product.
* @return float
*/
public function should_convert_product_price( $should_convert = true, $product = null ) {
global $woocommerce_smart_coupon;
if ( ! $product instanceof WC_Product ) {
return $should_convert;
}
$coupons = $woocommerce_smart_coupon->get_coupon_titles( array( 'product_object' => $product ) );
if ( ! empty( $coupons ) && $woocommerce_smart_coupon->is_coupon_amount_pick_from_product_price( $coupons ) ) {
foreach ( $coupons as $coupon_title ) {
$coupon_of_product = new WC_Coupon( $coupon_title );
$discount_type_of_product = ( is_object( $coupon_of_product ) && is_callable( array( $coupon_of_product, 'get_discount_type' ) ) ) ? $coupon_of_product->get_discount_type() : '';
if ( 'smart_coupon' === $discount_type_of_product ) {
return false;
}
}
}
return $should_convert;
}
}
}
WC_SC_WooPayments_Compatibility::get_instance();

View File

@@ -0,0 +1,72 @@
<?php
/**
* Compatibility file for WPML
*
* @author StoreApps
* @since 3.3.0
* @version 1.1.0
*
* @package woocommerce-smart-coupons/includes/
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
if ( ! class_exists( 'WC_SC_WPML_Compatibility' ) ) {
/**
* Class for handling compatibility with WPML
*/
class WC_SC_WPML_Compatibility {
/**
* Variable to hold instance of WC_SC_WPML_Compatibility
*
* @var $instance
*/
private static $instance = null;
/**
* Constructor
*/
public function __construct() {
add_action( 'init', array( $this, 'woocommerce_wpml_compatibility' ), 11 );
}
/**
* Get single instance of WC_SC_WPML_Compatibility
*
* @return WC_SC_WPML_Compatibility Singleton object of WC_SC_WPML_Compatibility
*/
public static function get_instance() {
// Check if instance is already exists.
if ( is_null( self::$instance ) ) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Function to handle compatibility with WooCommerce Multilingual
*/
public function woocommerce_wpml_compatibility() {
global $woocommerce_wpml;
if ( class_exists( 'woocommerce_wpml' ) && $woocommerce_wpml instanceof woocommerce_wpml ) {
if ( ! empty( $woocommerce_wpml->products ) && has_action( 'woocommerce_before_checkout_process', array( $woocommerce_wpml->products, 'wcml_refresh_cart_total' ) ) ) {
remove_action( 'woocommerce_before_checkout_process', array( $woocommerce_wpml->products, 'wcml_refresh_cart_total' ) );
}
if ( ! empty( $woocommerce_wpml->cart ) && has_action( 'woocommerce_before_checkout_process', array( $woocommerce_wpml->cart, 'wcml_refresh_cart_total' ) ) ) {
remove_action( 'woocommerce_before_checkout_process', array( $woocommerce_wpml->cart, 'wcml_refresh_cart_total' ) );
}
}
}
}
}
WC_SC_WPML_Compatibility::get_instance();

View File

@@ -0,0 +1,103 @@
<?php
/**
* Compatibility file for WooCommerce Side Cart Premium
*
* @author StoreApps
* @since 8.11.0
* @version 1.0.0
*
* @package woocommerce-smart-coupons/includes/compat/
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
if ( ! class_exists( 'WC_SC_WSCP_Compatibility' ) ) {
/**
* Class for handling compatibility with WooCommerce Side Cart Premium
*/
class WC_SC_WSCP_Compatibility {
/**
* Variable to hold instance of WC_SC_WSCP_Compatibility
*
* @var $instance
*/
private static $instance = null;
/**
* Get single instance of WC_SC_WSCP_Compatibility
*
* @return WC_SC_WSCP_Compatibility Singleton object of WC_SC_WSCP_Compatibility
*/
public static function get_instance() {
// Check if instance is already exists.
if ( is_null( self::$instance ) ) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Constructor
*/
private function __construct() {
add_action( 'wp_loaded', array( $this, 'hooks_for_compatibility' ) );
}
/**
* Handle call to functions which is not available in this class
*
* @param string $function_name Function to call.
* @param array $arguments Array of arguments passed while calling $function_name.
* @return mixed Result of function call.
*/
public function __call( $function_name, $arguments = array() ) {
global $woocommerce_smart_coupon;
if ( ! is_callable( array( $woocommerce_smart_coupon, $function_name ) ) ) {
return;
}
if ( ! empty( $arguments ) ) {
return call_user_func_array( array( $woocommerce_smart_coupon, $function_name ), $arguments );
} else {
return call_user_func( array( $woocommerce_smart_coupon, $function_name ) );
}
}
/**
* Add compatibility related functionality
*/
public function hooks_for_compatibility() {
if ( ! function_exists( 'is_plugin_active' ) ) {
require_once ABSPATH . 'wp-admin/includes/plugin.php';
}
if ( is_plugin_active( 'woocommerce-side-cart-premium/xoo-wsc-main.php' ) ) {
$apply_before_tax = get_option( 'woocommerce_smart_coupon_apply_before_tax', 'no' );
if ( ! class_exists( 'WC_SC_Apply_Before_Tax' ) && 'yes' === $apply_before_tax ) {
include_once plugin_dir_path( WC_SC_PLUGIN_FILE ) . 'includes/class-wc-sc-apply-before-tax.php';
}
if ( class_exists( 'WC_SC_Apply_Before_Tax' ) ) {
$wc_sc_apply_before_tax_instance = WC_SC_Apply_Before_Tax::get_instance();
add_action( 'woocommerce_ajax_added_to_cart', array( $wc_sc_apply_before_tax_instance, 'cart_calculate_discount_amount' ), 10 );
}
if ( class_exists( 'WC_SC_URL_Coupon' ) ) {
$wc_sc_url_coupon_instance = WC_SC_URL_Coupon::get_instance();
add_action( 'woocommerce_ajax_added_to_cart', array( $wc_sc_url_coupon_instance, 'apply_coupon_from_session' ) );
}
}
}
}
}
WC_SC_WSCP_Compatibility::get_instance();

View File

@@ -0,0 +1,166 @@
<?php
/**
* Compatibility file for WooCommerce One Page Checkout
*
* @author StoreApps
* @since 3.3.0
* @version 1.1.0
* @package WooCommerce Smart Coupons
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
if ( ! class_exists( 'WCOPC_SC_Compatibility' ) ) {
/**
* Class for handling compatibility with WooCommerce One Page Checkout
*/
class WCOPC_SC_Compatibility {
/**
* Variable to hold instance of WCOPC_SC_Compatibility
*
* @var $instance
*/
private static $instance = null;
/**
* Constructor
*/
public function __construct() {
add_filter( 'woocommerce_update_order_review_fragments', array( $this, 'update_order_review_fragments' ) );
add_action( 'wcopc_before_display_checkout', array( $this, 'add_styles_and_scripts' ) );
add_filter( 'wc_sc_call_for_credit_product_id', array( $this, 'call_for_credit_product_id' ), 10, 2 );
}
/**
* Get single instance of WCOPC_SC_Compatibility
*
* @return WCOPC_SC_Compatibility Singleton object of WCOPC_SC_Compatibility
*/
public static function get_instance() {
// Check if instance is already exists.
if ( is_null( self::$instance ) ) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Handle call to functions which is not available in this class
*
* @param string $function_name Function to call.
* @param array $arguments Array of arguments passed while calling $function_name.
* @return mixed Result of function call.
*/
public function __call( $function_name, $arguments = array() ) {
global $woocommerce_smart_coupon;
if ( ! is_callable( array( $woocommerce_smart_coupon, $function_name ) ) ) {
return;
}
if ( ! empty( $arguments ) ) {
return call_user_func_array( array( $woocommerce_smart_coupon, $function_name ), $arguments );
} else {
return call_user_func( array( $woocommerce_smart_coupon, $function_name ) );
}
}
/**
* Generate & add coupon receiver details form in update order review fragments
*
* @param array $fragments Existing fragments.
* @return array $fragments
*/
public function update_order_review_fragments( $fragments = array() ) {
if ( ! class_exists( 'WC_SC_Purchase_Credit' ) ) {
include_once '../class-wc-sc-purchase-credit.php';
}
$wc_sc_purchase_credit = WC_SC_Purchase_Credit::get_instance();
ob_start();
$wc_sc_purchase_credit->gift_certificate_receiver_detail_form();
$fragments['wc_sc_receiver_detail_form'] = ob_get_clean();
return $fragments;
}
/**
* Add Styles And Scripts
*/
public function add_styles_and_scripts() {
if ( ! class_exists( 'WC_SC_Purchase_Credit' ) ) {
include_once '../class-wc-sc-purchase-credit.php';
}
$wc_sc_purchase_credit = WC_SC_Purchase_Credit::get_instance();
$wc_sc_purchase_credit->enqueue_timepicker();
add_action( 'wp_footer', array( $this, 'styles_and_scripts' ) );
}
/**
* Styles And Scripts
*/
public function styles_and_scripts() {
if ( ! wp_script_is( 'jquery' ) ) {
wp_enqueue_script( 'jquery' );
}
?>
<script type="text/javascript">
jQuery(function(){
jQuery(document.body).on('updated_checkout', function( e, data ){
if ( typeof data === 'undefined' ) {
return;
}
if ( data.fragments.wc_sc_receiver_detail_form ) {
if ( jQuery('div.gift-certificate.sc_info_box').length > 0 ) {
jQuery('div.gift-certificate.sc_info_box').replaceWith( data.fragments.wc_sc_receiver_detail_form );
} else {
jQuery('div#customer_details').after( data.fragments.wc_sc_receiver_detail_form );
}
} else {
jQuery('div.gift-certificate.sc_info_box').remove();
}
});
});
</script>
<?php
}
/**
* Call For Credit Product Id
*
* @param integer $product_id The product id.
* @param array $args Additional arguments.
* @return integer
*/
public function call_for_credit_product_id( $product_id = 0, $args = array() ) {
$action = ( ! empty( $_REQUEST['action'] ) ) ? wc_clean( wp_unslash( $_REQUEST['action'] ) ) : ''; // phpcs:ignore
if ( 'pp_add_to_cart' === $action && empty( $product_id ) ) {
$product_id = ( ! empty( $_REQUEST['add_to_cart'] ) ) ? absint( $_REQUEST['add_to_cart'] ) : 0; // phpcs:ignore
}
return $product_id;
}
}
}
/**
* Initialize the Compatibility
*/
function initialize_wcopc_sc_compatibility() {
WCOPC_SC_Compatibility::get_instance();
}
add_action( 'wcopc_loaded', 'initialize_wcopc_sc_compatibility' );

View File

@@ -0,0 +1,350 @@
<?php
/**
* Main class for Smart Coupons Acknowledgement Email
*
* @author StoreApps
* @since 4.7.8
* @version 1.2.0
*
* @package woocommerce-smart-coupons/includes/emails/
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
if ( ! class_exists( 'WC_SC_Acknowledgement_Email' ) ) {
/**
* The Smart Coupons Email class
*
* @extends \WC_SC_Email
*/
class WC_SC_Acknowledgement_Email extends WC_SC_Email {
/**
* Whether email is a scheduled email or not.
*
* @var bool
*/
public $is_email_scheduled = false;
/**
* Set email defaults
*/
public function __construct() {
$this->id = 'wc_sc_acknowledgement_email';
$this->customer_email = true;
// Set email title and description.
$this->title = __( 'Smart Coupons - Acknowledgement email', 'woocommerce-smart-coupons' );
$this->description = __( 'Send an acknowledgement email to the purchaser. One email per customer.', 'woocommerce-smart-coupons' );
// Use our plugin templates directory as the template base.
$this->template_base = dirname( WC_SC_PLUGIN_FILE ) . '/templates/';
// Email template location.
$this->template_html = 'acknowledgement-email.php';
$this->template_plain = 'plain/acknowledgement-email.php';
$this->placeholders = array(
'{coupon_type}' => '',
);
// Trigger for this email.
add_action( 'wc_sc_acknowledgement_email_notification', array( $this, 'trigger' ) );
// Call parent constructor to load any other defaults not explicity defined here.
parent::__construct();
}
/**
* Get default email subject.
*
* @return string Default email subject
*/
public function get_default_subject() {
return __( '{site_title}: {coupon_type} sent successfully', 'woocommerce-smart-coupons' );
}
/**
* Get default email heading.
*
* @return string Default email heading
*/
public function get_default_heading() {
return __( '{coupon_type} sent successfully', 'woocommerce-smart-coupons' );
}
/**
* Get default scheduled email subject.
*
* @return string Default email subject
*/
public function get_default_scheduled_subject() {
return __( '{site_title}: {coupon_type} has been successfully scheduled', 'woocommerce-smart-coupons' );
}
/**
* Get default scheduled email heading.
*
* @return string Default email heading
*/
public function get_default_scheduled_heading() {
return __( '{coupon_type} has been successfully scheduled', 'woocommerce-smart-coupons' );
}
/**
* Initialize Settings Form Fields
*/
public function init_form_fields() {
/* translators: %s: list of placeholders */
$placeholder_text = sprintf( __( 'This will be used when the setting "WooCommerce > Settings > Smart Coupons > Allow schedule sending of coupons?" is enabled. Available placeholders: %s.', 'woocommerce-smart-coupons' ), '<code>' . implode( '</code>, <code>', array_keys( $this->placeholders ) ) . '</code>' );
$form_fields = array(
'scheduled_subject' => array(
'title' => __( 'Scheduled email subject', 'woocommerce-smart-coupons' ),
'type' => 'text',
'desc_tip' => true,
'description' => $placeholder_text,
'placeholder' => $this->get_default_scheduled_subject(),
'default' => '',
),
'scheduled_heading' => array(
'title' => __( 'Scheduled email heading', 'woocommerce-smart-coupons' ),
'type' => 'text',
'desc_tip' => true,
'description' => $placeholder_text,
'placeholder' => $this->get_default_scheduled_heading(),
'default' => '',
),
);
parent::init_form_fields();
$this->form_fields = array_merge( $this->form_fields, $form_fields );
}
/**
* Determine if the email should actually be sent and setup email merge variables
*
* @param array $args Email arguments.
*/
public function trigger( $args = array() ) {
$this->email_args = wp_parse_args( $args, $this->email_args );
if ( ! isset( $this->email_args['email'] ) || empty( $this->email_args['email'] ) ) {
return;
}
$email_scheduled_details = ! empty( $this->email_args['scheduled_email'] ) ? $this->email_args['scheduled_email'] : array();
$this->is_email_scheduled = ! empty( $email_scheduled_details );
$this->setup_locale();
$this->recipient = $this->email_args['email'];
$order_id = isset( $this->email_args['order_id'] ) ? $this->email_args['order_id'] : 0;
// Get order object.
if ( ! empty( $order_id ) && 0 !== $order_id ) {
$order = wc_get_order( $order_id );
if ( is_a( $order, 'WC_Order' ) ) {
$this->object = $order;
}
}
$this->set_placeholders();
$email_content = $this->get_content();
// Replace placeholders with values in the email content.
$email_content = ( is_callable( array( $this, 'format_string' ) ) ) ? $this->format_string( $email_content ) : $email_content;
// Send email.
if ( $this->is_enabled() && $this->get_recipient() ) {
$this->send( $this->get_recipient(), $this->get_subject(), $email_content, $this->get_headers(), $this->get_attachments() );
}
$this->restore_locale();
}
/**
* Function to get email subject.
*
* @return string Email subject.
*/
public function get_subject() {
if ( true === $this->is_email_scheduled ) {
return $this->get_scheduled_subject();
}
return parent::get_subject();
}
/**
* Function to get email subject.
*
* @return string Email subject.
*/
public function get_heading() {
if ( true === $this->is_email_scheduled ) {
return $this->get_scheduled_heading();
}
return parent::get_heading();
}
/**
* Get scheduled email subject.
*
* @return string
*/
public function get_scheduled_subject() {
return apply_filters(
$this->id . '_scheduled_subject',
$this->format_string( $this->get_option( 'scheduled_subject', $this->get_default_scheduled_subject() ) ),
array(
'email_object' => $this->object,
'source' => $this,
)
);
}
/**
* Get scheduled email heading.
*
* @return string
*/
public function get_scheduled_heading() {
return apply_filters(
$this->id . '_scheduled_heading',
$this->format_string( $this->get_option( 'scheduled_heading', $this->get_default_scheduled_heading() ) ),
array(
'email_object' => $this->object,
'source' => $this,
)
);
}
/**
* Function to set placeholder variables used in email subject/heading
*/
public function set_placeholders() {
$this->placeholders['{coupon_type}'] = $this->get_coupon_type();
}
/**
* Function to load email html content
*
* @return string Email content html
*/
public function get_content_html() {
global $woocommerce_smart_coupon;
$order = $this->object;
$email_heading = $this->get_heading();
$email = isset( $this->email_args['email'] ) ? $this->email_args['email'] : '';
$receivers_detail = isset( $this->email_args['receivers_detail'] ) ? $this->email_args['receivers_detail'] : array();
$receiver_name = isset( $this->email_args['receiver_name'] ) ? $this->email_args['receiver_name'] : '';
$receiver_count = isset( $this->email_args['receiver_count'] ) ? $this->email_args['receiver_count'] : 0;
$email_scheduled_details = isset( $this->email_args['scheduled_email'] ) ? $this->email_args['scheduled_email'] : array();
$contains_core_coupons = ( isset( $this->email_args['contains_core_coupons'] ) && 'yes' === $this->email_args['contains_core_coupons'] ) ? $this->email_args['contains_core_coupons'] : 'no';
$default_path = $this->template_base;
$template_path = $woocommerce_smart_coupon->get_template_base_dir( $this->template_html );
ob_start();
wc_get_template(
$this->template_html,
array(
'email' => $email,
'email_obj' => $this,
'email_heading' => $email_heading,
'order' => $order,
'receivers_detail' => $receivers_detail,
'gift_certificate_receiver_name' => $receiver_name,
'receiver_count' => $receiver_count,
'email_scheduled_details' => $email_scheduled_details,
'contains_core_coupons' => $contains_core_coupons,
),
$template_path,
$default_path
);
return ob_get_clean();
}
/**
* Function to load email plain content
*
* @return string Email plain content
*/
public function get_content_plain() {
global $woocommerce_smart_coupon;
$order = $this->object;
$email_heading = $this->get_heading();
$email = isset( $this->email_args['email'] ) ? $this->email_args['email'] : '';
$receivers_detail = isset( $this->email_args['receivers_detail'] ) ? $this->email_args['receivers_detail'] : array();
$receiver_name = isset( $this->email_args['receiver_name'] ) ? $this->email_args['receiver_name'] : '';
$receiver_count = isset( $this->email_args['receiver_count'] ) ? $this->email_args['receiver_count'] : 0;
$email_scheduled_details = isset( $this->email_args['scheduled_email'] ) ? $this->email_args['scheduled_email'] : array();
$contains_core_coupons = ( isset( $this->email_args['contains_core_coupons'] ) && 'yes' === $this->email_args['contains_core_coupons'] ) ? $this->email_args['contains_core_coupons'] : 'no';
$default_path = $this->template_base;
$template_path = $woocommerce_smart_coupon->get_template_base_dir( $this->template_html );
ob_start();
wc_get_template(
$this->template_plain,
array(
'email' => $email,
'email_obj' => $this,
'email_heading' => $email_heading,
'order' => $order,
'receivers_detail' => $receivers_detail,
'gift_certificate_receiver_name' => $receiver_name,
'receiver_count' => $receiver_count,
'email_scheduled_details' => $email_scheduled_details,
'contains_core_coupons' => $contains_core_coupons,
),
$template_path,
$default_path
);
return ob_get_clean();
}
/**
* Function to get coupon type for current coupon being sent.
*
* @return string $coupon_type Coupon type.
*/
public function get_coupon_type() {
global $store_credit_label;
$receiver_count = isset( $this->email_args['receiver_count'] ) ? $this->email_args['receiver_count'] : 0;
$singular = ( ! empty( $store_credit_label['singular'] ) ) ? ucwords( $store_credit_label['singular'] ) : __( 'Gift card', 'woocommerce-smart-coupons' );
$plural = ( ! empty( $store_credit_label['plural'] ) ) ? ucwords( $store_credit_label['plural'] ) : __( 'Gift cards', 'woocommerce-smart-coupons' );
$coupon_type = ( $receiver_count > 1 ) ? $plural : $singular;
$contains_core_coupons = ( isset( $this->email_args['contains_core_coupons'] ) && 'yes' === $this->email_args['contains_core_coupons'] ) ? $this->email_args['contains_core_coupons'] : 'no';
if ( 'yes' === $contains_core_coupons ) {
$coupon_type = _n( 'Coupon', 'Coupons', $receiver_count, 'woocommerce-smart-coupons' );
}
return $coupon_type;
}
}
}

View File

@@ -0,0 +1,267 @@
<?php
/**
* Main class for Smart Coupons Email
*
* @author StoreApps
* @since 4.4.1
* @version 1.3.0
*
* @package woocommerce-smart-coupons/includes/emails/
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
if ( ! class_exists( 'WC_SC_Combined_Email_Coupon' ) ) {
/**
* The Smart Coupons Combined Email class
*
* @extends \WC_SC_Email
*/
class WC_SC_Combined_Email_Coupon extends WC_SC_Email {
/**
* Set email defaults
*/
public function __construct() {
$this->id = 'wc_sc_combined_email_coupon';
$this->customer_email = true;
// Set email title and description.
$this->title = __( 'Smart Coupons - Combined auto generated coupons email', 'woocommerce-smart-coupons' );
$this->description = __( 'Send only one email instead of multiple emails when multiple coupons are generated per recipient.', 'woocommerce-smart-coupons' );
// Use our plugin templates directory as the template base.
$this->template_base = dirname( WC_SC_PLUGIN_FILE ) . '/templates/';
// Email template location.
$this->template_html = 'combined-email.php';
$this->template_plain = 'plain/combined-email.php';
$this->placeholders = array(
'{sender_name}' => '',
);
// Trigger for this email.
add_action( 'wc_sc_combined_email_coupon_notification', array( $this, 'trigger' ) );
// Call parent constructor to load any other defaults not explicity defined here.
parent::__construct();
}
/**
* Get default email subject.
*
* @return string Default email subject
*/
public function get_default_subject() {
return __( '{site_title}: Congratulations! You\'ve received coupons from {sender_name}', 'woocommerce-smart-coupons' );
}
/**
* Get default email heading.
*
* @return string Default email heading
*/
public function get_default_heading() {
return __( 'You have received coupons.', 'woocommerce-smart-coupons' );
}
/**
* Determine if the email should actually be sent and setup email merge variables
*
* @param array $args Email arguments.
*/
public function trigger( $args = array() ) {
$this->email_args = wp_parse_args( $args, $this->email_args );
if ( ! isset( $this->email_args['email'] ) || empty( $this->email_args['email'] ) ) {
return;
}
$this->setup_locale();
$this->recipient = $this->email_args['email'];
$order_id = isset( $this->email_args['order_id'] ) ? $this->email_args['order_id'] : 0;
// Get order object.
if ( ! empty( $order_id ) && 0 !== $order_id ) {
$order = wc_get_order( $order_id );
if ( is_a( $order, 'WC_Order' ) ) {
$this->object = $order;
}
}
$this->set_placeholders();
$email_content = $this->get_content();
// Replace placeholders with values in the email content.
$email_content = ( is_callable( array( $this, 'format_string' ) ) ) ? $this->format_string( $email_content ) : $email_content;
// Send email.
if ( $this->is_enabled() && $this->get_recipient() ) {
$this->send( $this->get_recipient(), $this->get_subject(), $email_content, $this->get_headers(), $this->get_attachments() );
}
$this->restore_locale();
}
/**
* Function to set placeholder variables used in email subject/heading
*/
public function set_placeholders() {
$this->placeholders['{sender_name}'] = $this->get_sender_name();
}
/**
* Function to load email html content
*
* @return string Email content html
*/
public function get_content_html() {
global $woocommerce_smart_coupon;
$order = $this->object;
$url = $this->get_url();
$email_heading = $this->get_heading();
$sender = '';
$from = '';
$is_gift = isset( $this->email_args['is_gift'] ) ? $this->email_args['is_gift'] : '';
if ( 'yes' === $is_gift ) {
$sender_name = $this->get_sender_name();
$sender_email = $this->get_sender_email();
if ( ! empty( $sender_name ) && ! empty( $sender_email ) ) {
$sender = $sender_name . ' (' . $sender_email . ') ';
$from = ' ' . __( 'from', 'woocommerce-smart-coupons' ) . ' ';
}
}
$email = isset( $this->email_args['email'] ) ? $this->email_args['email'] : '';
$receiver_details = isset( $this->email_args['receiver_details'] ) ? $this->email_args['receiver_details'] : '';
$design = get_option( 'wc_sc_setting_coupon_design', 'basic' );
$background_color = get_option( 'wc_sc_setting_coupon_background_color', '#39cccc' );
$foreground_color = get_option( 'wc_sc_setting_coupon_foreground_color', '#30050b' );
$third_color = get_option( 'wc_sc_setting_coupon_third_color', '#39cccc' );
$show_coupon_description = get_option( 'smart_coupons_show_coupon_description', 'no' );
$valid_designs = $woocommerce_smart_coupon->get_valid_coupon_designs();
if ( ! in_array( $design, $valid_designs, true ) ) {
$design = 'basic';
}
$design = ( 'custom-design' !== $design ) ? 'email-coupon' : $design;
$coupon_styles = $woocommerce_smart_coupon->get_coupon_styles( $design, array( 'is_email' => 'yes' ) );
$default_path = $this->template_base;
$template_path = $woocommerce_smart_coupon->get_template_base_dir( $this->template_html );
ob_start();
wc_get_template(
$this->template_html,
array(
'email' => $email,
'email_obj' => $this,
'email_heading' => $email_heading,
'order' => $order,
'url' => $url,
'from' => $from,
'background_color' => $background_color,
'foreground_color' => $foreground_color,
'third_color' => $third_color,
'coupon_styles' => $coupon_styles,
'sender' => $sender,
'receiver_details' => $receiver_details,
'show_coupon_description' => $show_coupon_description,
'design' => $design,
),
$template_path,
$default_path
);
return ob_get_clean();
}
/**
* Function to load email plain content
*
* @return string Email plain content
*/
public function get_content_plain() {
global $woocommerce_smart_coupon;
$order = $this->object;
$url = $this->get_url();
$email_heading = $this->get_heading();
$sender = '';
$from = '';
$is_gift = isset( $this->email_args['is_gift'] ) ? $this->email_args['is_gift'] : '';
if ( 'yes' === $is_gift ) {
$sender_name = $this->get_sender_name();
$sender_email = $this->get_sender_email();
if ( ! empty( $sender_name ) && ! empty( $sender_email ) ) {
$sender = $sender_name . ' (' . $sender_email . ') ';
$from = ' ' . __( 'from', 'woocommerce-smart-coupons' ) . ' ';
}
}
$email = isset( $this->email_args['email'] ) ? $this->email_args['email'] : '';
$receiver_details = isset( $this->email_args['receiver_details'] ) ? $this->email_args['receiver_details'] : '';
$default_path = $this->template_base;
$template_path = $woocommerce_smart_coupon->get_template_base_dir( $this->template_plain );
ob_start();
wc_get_template(
$this->template_plain,
array(
'email' => $email,
'email_obj' => $this,
'email_heading' => $email_heading,
'order' => $order,
'url' => $url,
'from' => $from,
'sender' => $sender,
'receiver_details' => $receiver_details,
),
$template_path,
$default_path
);
return ob_get_clean();
}
/**
* Function to update SC admin email settings when WC email settings get updated
*/
public function process_admin_options() {
// Save regular options.
parent::process_admin_options();
$is_email_enabled = $this->get_field_value( 'enabled', $this->form_fields['enabled'] );
if ( ! empty( $is_email_enabled ) ) {
update_option( 'smart_coupons_combine_emails', $is_email_enabled, 'no' );
}
}
}
}

View File

@@ -0,0 +1,508 @@
<?php
/**
* Main class for Smart Coupons Email
*
* @author StoreApps
* @since 4.4.1
* @version 1.7.0
*
* @package woocommerce-smart-coupons/includes/emails/
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
if ( ! class_exists( 'WC_SC_Email_Coupon' ) ) {
/**
* The Smart Coupons Email class
*
* @extends \WC_SC_Email
*/
class WC_SC_Email_Coupon extends WC_SC_Email {
/**
* Set email defaults
*/
public function __construct() {
$this->id = 'wc_sc_email_coupon';
$this->customer_email = true;
// Set email title and description.
$this->title = __( 'Smart Coupons - Auto generated coupon email', 'woocommerce-smart-coupons' );
$this->description = __( 'Email auto generated coupon to recipients. One email per coupon.', 'woocommerce-smart-coupons' );
// Use our plugin templates directory as the template base.
$this->template_base = dirname( WC_SC_PLUGIN_FILE ) . '/templates/';
// Email template location.
$this->template_html = 'email.php';
$this->template_plain = 'plain/email.php';
$this->placeholders = array(
'{coupon_code}' => '',
'{coupon_type}' => '',
'{coupon_value}' => '',
'{sender_name}' => '',
);
// Trigger for this email.
add_action( 'wc_sc_email_coupon_notification', array( $this, 'trigger' ) );
// Call parent constructor to load any other defaults not explicity defined here.
parent::__construct();
}
/**
* Get default email subject.
*
* @return string Default email subject
*/
public function get_default_subject() {
return __( '{site_title}: Congratulations! You\'ve received a {coupon_type} from {sender_name}', 'woocommerce-smart-coupons' );
}
/**
* Get default email heading.
*
* @return string Default email heading
*/
public function get_default_heading() {
return __( 'You have received a {coupon_type} {coupon_value}', 'woocommerce-smart-coupons' );
}
/**
* Determine if the email should actually be sent and setup email merge variables
*
* @param array $args Email arguments.
*/
public function trigger( $args = array() ) {
$this->email_args = wp_parse_args( $args, $this->email_args );
if ( ! isset( $this->email_args['email'] ) || empty( $this->email_args['email'] ) ) {
return;
}
$this->setup_locale();
$this->recipient = $this->email_args['email'];
$order_id = isset( $this->email_args['order_id'] ) ? $this->email_args['order_id'] : 0;
// Get order object.
if ( ! empty( $order_id ) && 0 !== $order_id ) {
$order = wc_get_order( $order_id );
if ( is_a( $order, 'WC_Order' ) ) {
$this->object = $order;
}
}
$this->set_placeholders();
$email_content = $this->get_content();
// Replace placeholders with values in the email content.
$email_content = ( is_callable( array( $this, 'format_string' ) ) ) ? $this->format_string( $email_content ) : $email_content;
// Send email.
if ( $this->is_enabled() && $this->get_recipient() ) {
$this->send( $this->get_recipient(), $this->get_subject(), $email_content, $this->get_headers(), $this->get_attachments() );
}
$this->restore_locale();
}
/**
* Function to set placeholder variables used in email subject/heading
*/
public function set_placeholders() {
$this->placeholders['{coupon_code}'] = $this->get_coupon_code();
$this->placeholders['{coupon_type}'] = $this->get_coupon_type();
$this->placeholders['{coupon_value}'] = $this->get_coupon_value();
$this->placeholders['{coupon_expiry}'] = $this->get_coupon_expiry();
$this->placeholders['{sender_name}'] = $this->get_sender_name();
}
/**
* Function to get coupon expiry date/time for current coupon being sent.
*
* @return string $coupon_expiry Coupon expiry.
*/
public function get_coupon_expiry() {
global $woocommerce_smart_coupon;
$coupon_expiry = '';
$coupon = isset( $this->email_args['coupon'] ) ? $this->email_args['coupon'] : '';
if ( empty( $coupon ) ) {
return $coupon_expiry;
}
$coupon_code = ( ! empty( $coupon['code'] ) ) ? $coupon['code'] : '';
if ( empty( $coupon_code ) ) {
return $coupon_expiry;
}
$_coupon = new WC_Coupon( $coupon_code );
if ( $woocommerce_smart_coupon->is_wc_gte_30() ) {
$coupon_id = ( is_object( $_coupon ) && is_callable( array( $_coupon, 'get_id' ) ) ) ? $_coupon->get_id() : 0;
$expiry_date = ( is_object( $_coupon ) && is_callable( array( $_coupon, 'get_date_expires' ) ) ) ? $_coupon->get_date_expires() : '';
} else {
$coupon_id = ( ! empty( $_coupon->id ) ) ? $_coupon->id : 0;
$expiry_date = ( ! empty( $_coupon->expiry_date ) ) ? $_coupon->expiry_date : '';
}
if ( ! empty( $expiry_date ) ) {
if ( $woocommerce_smart_coupon->is_wc_gte_30() && $expiry_date instanceof WC_DateTime ) {
$expiry_date = ( is_callable( array( $expiry_date, 'getTimestamp' ) ) ) ? $expiry_date->getTimestamp() : null;
} elseif ( ! is_int( $expiry_date ) ) {
$expiry_date = strtotime( $expiry_date );
}
if ( ! empty( $expiry_date ) && is_int( $expiry_date ) ) {
$expiry_time = (int) $woocommerce_smart_coupon->get_post_meta( $coupon_id, 'wc_sc_expiry_time', true );
if ( ! empty( $expiry_time ) ) {
$expiry_date += $expiry_time; // Adding expiry time to expiry date.
}
}
$coupon_expiry = $woocommerce_smart_coupon->get_expiration_format( $expiry_date );
} else {
$coupon_expiry = esc_html__( 'Never expires', 'woocommerce-smart-coupons' );
}
return $coupon_expiry;
}
/**
* Function to load email html content
*
* @return string Email content html
*/
public function get_content_html() {
global $woocommerce_smart_coupon;
$order = $this->object;
$url = $this->get_url();
$email_heading = $this->get_heading();
$sender = '';
$from = '';
$is_gift = isset( $this->email_args['is_gift'] ) ? $this->email_args['is_gift'] : '';
if ( 'yes' === $is_gift ) {
$sender_name = $this->get_sender_name();
$sender_email = $this->get_sender_email();
if ( ! empty( $sender_name ) && ! empty( $sender_email ) ) {
$sender = $sender_name . ' (' . $sender_email . ') ';
$from = ' ' . __( 'from', 'woocommerce-smart-coupons' ) . ' ';
}
}
$email = isset( $this->email_args['email'] ) ? $this->email_args['email'] : '';
$message_from_sender = isset( $this->email_args['message_from_sender'] ) ? $this->email_args['message_from_sender'] : '';
$coupon_code = isset( $this->email_args['coupon']['code'] ) ? $this->email_args['coupon']['code'] : '';
$design = get_option( 'wc_sc_setting_coupon_design', 'basic' );
$background_color = get_option( 'wc_sc_setting_coupon_background_color', '#39cccc' );
$foreground_color = get_option( 'wc_sc_setting_coupon_foreground_color', '#30050b' );
$third_color = get_option( 'wc_sc_setting_coupon_third_color', '#39cccc' );
$show_coupon_description = get_option( 'smart_coupons_show_coupon_description', 'no' );
$valid_designs = $woocommerce_smart_coupon->get_valid_coupon_designs();
if ( ! in_array( $design, $valid_designs, true ) ) {
$design = 'basic';
}
$design = ( 'custom-design' !== $design ) ? 'email-coupon' : $design;
$coupon_styles = $woocommerce_smart_coupon->get_coupon_styles( $design, array( 'is_email' => 'yes' ) );
$default_path = $this->template_base;
$template_path = $woocommerce_smart_coupon->get_template_base_dir( $this->template_html );
ob_start();
wc_get_template(
$this->template_html,
array(
'email' => $email,
'email_obj' => $this,
'email_heading' => $email_heading,
'order' => $order,
'url' => $url,
'message_from_sender' => $message_from_sender,
'from' => $from,
'coupon_code' => $coupon_code,
'background_color' => $background_color,
'foreground_color' => $foreground_color,
'third_color' => $third_color,
'coupon_styles' => $coupon_styles,
'sender' => $sender,
'design' => $design,
'show_coupon_description' => $show_coupon_description,
),
$template_path,
$default_path
);
return ob_get_clean();
}
/**
* Function to load email plain content
*
* @return string Email plain content
*/
public function get_content_plain() {
global $woocommerce_smart_coupon;
$order = $this->object;
$url = $this->get_url();
$email_heading = $this->get_heading();
$sender = '';
$from = '';
$is_gift = isset( $this->email_args['is_gift'] ) ? $this->email_args['is_gift'] : '';
if ( 'yes' === $is_gift ) {
$sender_name = $this->get_sender_name();
$sender_email = $this->get_sender_email();
if ( ! empty( $sender_name ) && ! empty( $sender_email ) ) {
$sender = $sender_name . ' (' . $sender_email . ') ';
$from = ' ' . __( 'from', 'woocommerce-smart-coupons' ) . ' ';
}
}
$email = isset( $this->email_args['email'] ) ? $this->email_args['email'] : '';
$message_from_sender = isset( $this->email_args['message_from_sender'] ) ? $this->email_args['message_from_sender'] : '';
$coupon_code = isset( $this->email_args['coupon']['code'] ) ? $this->email_args['coupon']['code'] : '';
$default_path = $this->template_base;
$template_path = $woocommerce_smart_coupon->get_template_base_dir( $this->template_plain );
ob_start();
wc_get_template(
$this->template_plain,
array(
'email' => $email,
'email_obj' => $this,
'email_heading' => $email_heading,
'order' => $order,
'url' => $url,
'message_from_sender' => $message_from_sender,
'from' => $from,
'coupon_code' => $coupon_code,
'sender' => $sender,
),
$template_path,
$default_path
);
return ob_get_clean();
}
/**
* Get coupon code
*
* @return string
*/
public function get_coupon_code() {
$coupon_code = isset( $this->email_args['coupon']['code'] ) ? $this->email_args['coupon']['code'] : '';
return $coupon_code;
}
/**
* Get coupon value
*
* @return string
*/
public function get_coupon_value() {
global $woocommerce_smart_coupon, $store_credit_label;
$coupon = isset( $this->email_args['coupon'] ) ? $this->email_args['coupon'] : '';
if ( empty( $coupon ) ) {
return '';
}
$order_id = isset( $this->email_args['order_id'] ) ? $this->email_args['order_id'] : 0;
$order = ( ! empty( $order_id ) ) ? wc_get_order( $order_id ) : null;
$wc_price_args = array(
'currency' => is_callable( array( $order, 'get_currency' ) ) ? $order->get_currency() : '',
);
$discount_type = isset( $this->email_args['discount_type'] ) ? $this->email_args['discount_type'] : '';
$is_gift = isset( $this->email_args['is_gift'] ) ? $this->email_args['is_gift'] : '';
// Get smart coupon type string.
if ( 'smart_coupon' === $discount_type && 'yes' === $is_gift ) {
$smart_coupon_type = __( 'Gift Card', 'woocommerce-smart-coupons' );
} else {
$smart_coupon_type = __( 'Store Credit', 'woocommerce-smart-coupons' );
}
if ( ! empty( $store_credit_label['singular'] ) ) {
$smart_coupon_type = ucwords( $store_credit_label['singular'] );
}
$amount = $coupon['amount'];
$coupon_code = $coupon['code'];
// Get coupon types.
$all_discount_types = wc_get_coupon_types();
$_coupon = new WC_Coupon( $coupon_code );
if ( $woocommerce_smart_coupon->is_wc_gte_30() ) {
$_coupon_id = ( is_object( $_coupon ) && is_callable( array( $_coupon, 'get_id' ) ) ) ? $_coupon->get_id() : 0;
$_is_free_shipping = ( $_coupon->get_free_shipping() ) ? 'yes' : 'no';
$_discount_type = $_coupon->get_discount_type();
$_product_ids = $_coupon->get_product_ids();
$_excluded_product_ids = $_coupon->get_excluded_product_ids();
$_product_categories = $_coupon->get_product_categories();
$_excluded_product_categories = $_coupon->get_excluded_product_categories();
} else {
$_coupon_id = ( ! empty( $_coupon->id ) ) ? $_coupon->id : 0;
$_is_free_shipping = ( ! empty( $_coupon->free_shipping ) ) ? $_coupon->free_shipping : '';
$_discount_type = ( ! empty( $_coupon->discount_type ) ) ? $_coupon->discount_type : '';
$_product_ids = ( ! empty( $_coupon->product_ids ) ) ? $_coupon->product_ids : array();
$_excluded_product_ids = ( ! empty( $_coupon->exclude_product_ids ) ) ? $_coupon->exclude_product_ids : array();
$_product_categories = ( ! empty( $_coupon->product_categories ) ) ? $_coupon->product_categories : array();
$_excluded_product_categories = ( ! empty( $_coupon->exclude_product_categories ) ) ? $_coupon->exclude_product_categories : array();
}
if ( false === stripos( $discount_type, 'percent' ) ) {
$amount = $woocommerce_smart_coupon->read_price( $amount, true, $order );
}
switch ( $discount_type ) {
case 'smart_coupon':
/* translators: %s coupon amount */
$coupon_value = sprintf( __( 'worth %s ', 'woocommerce-smart-coupons' ), wc_price( $amount, $wc_price_args ) );
break;
case 'fixed_cart':
/* translators: %s: coupon amount */
$coupon_value = sprintf( __( 'worth %s (for entire purchase) ', 'woocommerce-smart-coupons' ), wc_price( $amount, $wc_price_args ) );
break;
case 'fixed_product':
if ( ! empty( $_product_ids ) || ! empty( $_excluded_product_ids ) || ! empty( $_product_categories ) || ! empty( $_excluded_product_categories ) ) {
$_discount_for_text = __( 'for some products', 'woocommerce-smart-coupons' );
} else {
$_discount_for_text = __( 'for all products', 'woocommerce-smart-coupons' );
}
/* translators: 1: coupon amount 2: discount for text */
$coupon_value = sprintf( __( 'worth %1$s (%2$s) ', 'woocommerce-smart-coupons' ), wc_price( $amount, $wc_price_args ), $_discount_for_text );
break;
case 'percent_product':
if ( ! empty( $_product_ids ) || ! empty( $_excluded_product_ids ) || ! empty( $_product_categories ) || ! empty( $_excluded_product_categories ) ) {
$_discount_for_text = __( 'for some products', 'woocommerce-smart-coupons' );
} else {
$_discount_for_text = __( 'for all products', 'woocommerce-smart-coupons' );
}
/* translators: 1: coupon amount 2: discount for text */
$coupon_value = sprintf( __( 'worth %1$s%% (%2$s) ', 'woocommerce-smart-coupons' ), $amount, $_discount_for_text );
break;
case 'percent':
if ( ! empty( $_product_ids ) || ! empty( $_excluded_product_ids ) || ! empty( $_product_categories ) || ! empty( $_excluded_product_categories ) ) {
$_discount_for_text = __( 'for some products', 'woocommerce-smart-coupons' );
} else {
$_discount_for_text = __( 'for entire purchase', 'woocommerce-smart-coupons' );
}
$max_discount_text = '';
$max_discount = $woocommerce_smart_coupon->get_post_meta( $_coupon_id, 'wc_sc_max_discount', true, true, $order );
if ( ! empty( $max_discount ) && is_numeric( $max_discount ) ) {
/* translators: %s: Maximum coupon discount amount */
$max_discount_text = sprintf( __( ' upto %s', 'woocommerce-smart-coupons' ), wc_price( $max_discount, $wc_price_args ) );
}
/* translators: 1: coupon amount 2: max discount text 3: discount for text */
$coupon_value = sprintf( __( 'worth %1$s%% %2$s (%3$s) ', 'woocommerce-smart-coupons' ), $amount, $max_discount_text, $_discount_for_text );
break;
default:
$default_coupon_type = ( ! empty( $all_discount_types[ $discount_type ] ) ) ? $all_discount_types[ $discount_type ] : ucwords( str_replace( array( '_', '-' ), ' ', $discount_type ) );
$coupon_type = apply_filters( 'wc_sc_coupon_type', $default_coupon_type, $_coupon, $all_discount_types );
$coupon_amount = apply_filters( 'wc_sc_coupon_amount', $amount, $_coupon );
/* translators: 1: coupon type 2: coupon amount */
$coupon_value = sprintf( __( '%1$s coupon of %2$s', 'woocommerce-smart-coupons' ), $coupon_type, $coupon_amount );
$coupon_value = apply_filters( 'wc_sc_email_heading', $coupon_value, $_coupon );
break;
}
if ( 'yes' === $_is_free_shipping && in_array( $_discount_type, array( 'fixed_cart', 'fixed_product', 'percent_product', 'percent' ), true ) ) {
/* translators: 1: email heading 2: suffix */
$coupon_value = sprintf( __( '%1$s Free Shipping%2$s', 'woocommerce-smart-coupons' ), ( ( ! empty( $amount ) ) ? $coupon_value . __( '&', 'woocommerce-smart-coupons' ) : __( 'You have received a', 'woocommerce-smart-coupons' ) ), ( ( empty( $amount ) ) ? __( ' coupon', 'woocommerce-smart-coupons' ) : '' ) );
}
return wp_strip_all_tags( $coupon_value );
}
/**
* Function to get coupon type for current coupon being sent.
*
* @return string $coupon_type Coupon type.
*/
public function get_coupon_type() {
global $store_credit_label;
$discount_type = isset( $this->email_args['discount_type'] ) ? $this->email_args['discount_type'] : '';
$is_gift = isset( $this->email_args['is_gift'] ) ? $this->email_args['is_gift'] : '';
if ( 'smart_coupon' === $discount_type && 'yes' === $is_gift ) {
$smart_coupon_type = __( 'Gift Card', 'woocommerce-smart-coupons' );
} else {
$smart_coupon_type = __( 'Store Credit', 'woocommerce-smart-coupons' );
}
if ( ! empty( $store_credit_label['singular'] ) ) {
$smart_coupon_type = ucwords( $store_credit_label['singular'] );
}
$coupon_type = ( 'smart_coupon' === $discount_type && ! empty( $smart_coupon_type ) ) ? $smart_coupon_type : __( 'coupon', 'woocommerce-smart-coupons' );
return $coupon_type;
}
/**
* Function to update SC admin email settings when WC email settings get updated
*/
public function process_admin_options() {
// Save regular options.
parent::process_admin_options();
$is_email_enabled = $this->get_field_value( 'enabled', $this->form_fields['enabled'] );
if ( ! empty( $is_email_enabled ) ) {
update_option( 'smart_coupons_is_send_email', $is_email_enabled, 'no' );
}
}
}
}

View File

@@ -0,0 +1,149 @@
<?php
/**
* Main class for Smart Coupons Email
*
* @author StoreApps
* @since 4.4.1
* @version 1.2.1
*
* @package woocommerce-smart-coupons/includes/emails/
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
if ( ! class_exists( 'WC_SC_Email' ) ) {
/**
* The Smart Coupons Email class
*
* @extends \WC_Email
*/
class WC_SC_Email extends WC_Email {
/**
* Email args defaults
*
* @var array
*/
public $email_args = array(
'email' => '',
'coupon' => array(),
'discount_type' => 'smart_coupon',
'smart_coupon_type' => '',
'receiver_name' => '',
'message_from_sender' => '',
'gift_certificate_sender_name' => '',
'gift_certificate_sender_email' => '',
'from' => '',
'sender' => '',
'is_gift' => false,
);
/**
* Get shop page url
*
* @return string $url Shop page url
*/
public function get_url() {
global $woocommerce_smart_coupon;
if ( $woocommerce_smart_coupon->is_wc_gte_30() ) {
$page_id = wc_get_page_id( 'shop' );
} else {
$page_id = woocommerce_get_page_id( 'shop' );
}
$url = ( get_option( 'permalink_structure' ) ) ? get_permalink( $page_id ) : get_post_type_archive_link( 'product' );
return $url;
}
/**
* Function to get sender name.
*
* @return string $sender_name Sender name.
*/
public function get_sender_name() {
if ( isset( $this->email_args['gift_certificate_sender_name'] ) && ! empty( $this->email_args['gift_certificate_sender_name'] ) ) {
$sender_name = $this->email_args['gift_certificate_sender_name'];
} else {
$sender_name = is_callable( array( $this, 'get_blogname' ) ) ? $this->get_blogname() : '';
}
return $sender_name;
}
/**
* Function to get sender email.
*
* @return string $sender_email Sender email.
*/
public function get_sender_email() {
$sender_email = isset( $this->email_args['gift_certificate_sender_email'] ) ? $this->email_args['gift_certificate_sender_email'] : '';
return $sender_email;
}
/**
* Initialize Settings Form Fields
*/
public function init_form_fields() {
/* translators: %s: list of placeholders */
$placeholder_text = sprintf( __( 'Available placeholders: %s', 'woocommerce-smart-coupons' ), '<code>' . implode( '</code>, <code>', array_keys( $this->placeholders ) ) . '</code>' );
$this->form_fields = array(
'enabled' => array(
'title' => __( 'Enable/Disable', 'woocommerce-smart-coupons' ),
'type' => 'checkbox',
'label' => __( 'Enable this email notification', 'woocommerce-smart-coupons' ),
'default' => 'yes',
),
'email_type' => array(
'title' => __( 'Email type', 'woocommerce-smart-coupons' ),
'type' => 'select',
'description' => __( 'Choose which format of email to send.', 'woocommerce-smart-coupons' ),
'default' => 'html',
'class' => 'email_type wc-enhanced-select',
'options' => $this->get_email_type_options(),
'desc_tip' => true,
),
'subject' => array(
'title' => __( 'Subject', 'woocommerce-smart-coupons' ),
'type' => 'text',
'desc_tip' => true,
'description' => $placeholder_text,
'placeholder' => $this->get_default_subject(),
'default' => '',
),
'heading' => array(
'title' => __( 'Email heading', 'woocommerce-smart-coupons' ),
'type' => 'text',
'desc_tip' => true,
'description' => $placeholder_text,
'placeholder' => $this->get_default_heading(),
'default' => '',
),
);
}
/**
* Function to update SC admin email settings when WC email settings get updated
*/
public function process_admin_options() {
// Save regular options.
parent::process_admin_options();
$is_email_enabled = $this->get_field_value( 'enabled', $this->form_fields['enabled'] );
if ( ! empty( $is_email_enabled ) ) {
update_option( 'smart_coupons_is_send_email', $is_email_enabled, 'no' );
}
}
}
}

View File

@@ -0,0 +1,69 @@
<?php
// phpcs:ignoreFile
/**
* Plugin Name: Action Scheduler
* Plugin URI: https://actionscheduler.org
* Description: A robust scheduling library for use in WordPress plugins.
* Author: Automattic
* Author URI: https://automattic.com/
* Version: 3.7.0
* License: GPLv3
* Tested up to: 6.4
* Requires at least: 5.2
* Requires PHP: 5.6
*
* Copyright 2019 Automattic, Inc. (https://automattic.com/contact/)
*
* This program 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
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* @package ActionScheduler
*/
if ( ! function_exists( 'action_scheduler_register_3_dot_7_dot_0' ) && function_exists( 'add_action' ) ) { // WRCS: DEFINED_VERSION.
if ( ! class_exists( 'ActionScheduler_Versions', false ) ) {
require_once __DIR__ . '/classes/ActionScheduler_Versions.php';
add_action( 'plugins_loaded', array( 'ActionScheduler_Versions', 'initialize_latest_version' ), 1, 0 );
}
add_action( 'plugins_loaded', 'action_scheduler_register_3_dot_7_dot_0', 0, 0 ); // WRCS: DEFINED_VERSION.
/**
* Registers this version of Action Scheduler.
*/
function action_scheduler_register_3_dot_7_dot_0() { // WRCS: DEFINED_VERSION.
$versions = ActionScheduler_Versions::instance();
$versions->register( '3.7.0', 'action_scheduler_initialize_3_dot_7_dot_0' ); // WRCS: DEFINED_VERSION.
}
/**
* Initializes this version of Action Scheduler.
*/
function action_scheduler_initialize_3_dot_7_dot_0() { // WRCS: DEFINED_VERSION.
// A final safety check is required even here, because historic versions of Action Scheduler
// followed a different pattern (in some unusual cases, we could reach this point and the
// ActionScheduler class is already defined—so we need to guard against that).
if ( ! class_exists( 'ActionScheduler', false ) ) {
require_once __DIR__ . '/classes/abstracts/ActionScheduler.php';
ActionScheduler::init( __FILE__ );
}
}
// Support usage in themes - load this version if no plugin has loaded a version yet.
if ( did_action( 'plugins_loaded' ) && ! doing_action( 'plugins_loaded' ) && ! class_exists( 'ActionScheduler', false ) ) {
action_scheduler_initialize_3_dot_7_dot_0(); // WRCS: DEFINED_VERSION.
do_action( 'action_scheduler_pre_theme_init' );
ActionScheduler_Versions::initialize_latest_version();
}
}

View File

@@ -0,0 +1,135 @@
*** Changelog ***
= 3.7.0 - 2023-11-20 =
* Add extended indexes for hook_status_scheduled_date_gmt and status_sheduled_date_gmt.
* Catch and log exceptions thrown when actions can't be created, e.g. under a corrupt database schema.
* Release/3.6.4.
* Tweak - WP 6.4 compatibility.
* Update unit tests for upcoming dependency version policy.
* make sure hook action_scheduler_failed_execution can access original exception object.
* mention dependency version policy in usage.md.
= 3.6.4 - 2023-10-11 =
* Performance improvements when bulk cancelling actions.
* Dev-related fixes.
= 3.6.3 - 2023-09-13 =
* Use `_doing_it_wrong` in initialization check.
= 3.6.2 - 2023-08-09 =
* Add guidance about passing arguments.
* Atomic option locking.
* Improve bulk delete handling.
* Include database error in the exception message.
* Tweak - WP 6.3 compatibility.
= 3.6.1 - 2023-06-14 =
* Document new optional `$priority` arg for various API functions.
* Document the new `--exclude-groups` WP CLI option.
* Document the new `action_scheduler_init` hook.
* Ensure actions within each claim are executed in the expected order.
* Fix incorrect text domain.
* Remove SHOW TABLES usage when checking if tables exist.
= 3.6.0 - 2023-05-10 =
* Add $unique parameter to function signatures.
* Add a cast-to-int for extra safety before forming new DateTime object.
* Add a hook allowing exceptions for consistently failing recurring actions.
* Add action priorities.
* Add init hook.
* Always raise the time limit.
* Bump minimatch from 3.0.4 to 3.0.8.
* Bump yaml from 2.2.1 to 2.2.2.
* Defensive coding relating to gaps in declared schedule types.
* Do not process an action if it cannot be set to `in-progress`.
* Filter view labels (status names) should be translatable | #919.
* Fix WPCLI progress messages.
* Improve data-store initialization flow.
* Improve error handling across all supported PHP versions.
* Improve logic for flushing the runtime cache.
* Support exclusion of multiple groups.
* Update lint-staged and Node/NPM requirements.
* add CLI clean command.
* add CLI exclude-group filter.
* exclude past-due from list table all filter count.
* throwing an exception if as_schedule_recurring_action interval param is not of type integer.
= 3.5.4 - 2023-01-17 =
* Add pre filters during action registration.
* Async scheduling.
* Calculate timeouts based on total actions.
* Correctly order the parameters for `ActionScheduler_ActionFactory`'s calls to `single_unique`.
* Fetch action in memory first before releasing claim to avoid deadlock.
* PHP 8.2: declare property to fix creation of dynamic property warning.
* PHP 8.2: fix "Using ${var} in strings is deprecated, use {$var} instead".
* Prevent `undefined variable` warning for `$num_pastdue_actions`.
= 3.5.3 - 2022-11-09 =
* Query actions with partial match.
= 3.5.2 - 2022-09-16 =
* Fix - erroneous 3.5.1 release.
= 3.5.1 - 2022-09-13 =
* Maintenance on A/S docs.
* fix: PHP 8.2 deprecated notice.
= 3.5.0 - 2022-08-25 =
* Add - The active view link within the "Tools > Scheduled Actions" screen is now clickable.
* Add - A warning when there are past-due actions.
* Enhancement - Added the ability to schedule unique actions via an atomic operation.
* Enhancement - Improvements to cache invalidation when processing batches (when running on WordPress 6.0+).
* Enhancement - If a recurring action is found to be consistently failing, it will stop being rescheduled.
* Enhancement - Adds a new "Past Due" view to the scheduled actions list table.
= 3.4.2 - 2022-06-08 =
* Fix - Change the include for better linting.
* Fix - update: Added Action scheduler completed action hook.
= 3.4.1 - 2022-05-24 =
* Fix - Change the include for better linting.
* Fix - Fix the documented return type.
= 3.4.0 - 2021-10-29 =
* Enhancement - Number of items per page can now be set for the Scheduled Actions view (props @ovidiul). #771
* Fix - Do not lower the max_execution_time if it is already set to 0 (unlimited) (props @barryhughes). #755
* Fix - Avoid triggering autoloaders during the version resolution process (props @olegabr). #731 & #776
* Dev - ActionScheduler_wcSystemStatus PHPCS fixes (props @ovidiul). #761
* Dev - ActionScheduler_DBLogger.php PHPCS fixes (props @ovidiul). #768
* Dev - Fixed phpcs for ActionScheduler_Schedule_Deprecated (props @ovidiul). #762
* Dev - Improve actions table indicies (props @glagonikas). #774 & #777
* Dev - PHPCS fixes for ActionScheduler_DBStore.php (props @ovidiul). #769 & #778
* Dev - PHPCS Fixes for ActionScheduler_Abstract_ListTable (props @ovidiul). #763 & #779
* Dev - Adds new filter action_scheduler_claim_actions_order_by to allow tuning of the claim query (props @glagonikas). #773
* Dev - PHPCS fixes for ActionScheduler_WpPostStore class (props @ovidiul). #780
= 3.3.0 - 2021-09-15 =
* Enhancement - Adds as_has_scheduled_action() to provide a performant way to test for existing actions. #645
* Fix - Improves compatibility with environments where NO_ZERO_DATE is enabled. #519
* Fix - Adds safety checks to guard against errors when our database tables cannot be created. #645
* Dev - Now supports queries that use multiple statuses. #649
* Dev - Minimum requirements for WordPress and PHP bumped (to 5.2 and 5.6 respectively). #723
= 3.2.1 - 2021-06-21 =
* Fix - Add extra safety/account for different versions of AS and different loading patterns. #714
* Fix - Handle hidden columns (Tools → Scheduled Actions) | #600.
= 3.2.0 - 2021-06-03 =
* Fix - Add "no ordering" option to as_next_scheduled_action().
* Fix - Add secondary scheduled date checks when claiming actions (DBStore) | #634.
* Fix - Add secondary scheduled date checks when claiming actions (wpPostStore) | #634.
* Fix - Adds a new index to the action table, reducing the potential for deadlocks (props: @glagonikas).
* Fix - Fix unit tests infrastructure and adapt tests to PHP 8.
* Fix - Identify in-use data store.
* Fix - Improve test_migration_is_scheduled.
* Fix - PHP notice on list table.
* Fix - Speed up clean up and batch selects.
* Fix - Update pending dependencies.
* Fix - [PHP 8.0] Only pass action arg values through to do_action_ref_array().
* Fix - [PHP 8] Set the PHP version to 7.1 in composer.json for PHP 8 compatibility.
* Fix - add is_initialized() to docs.
* Fix - fix file permissions.
* Fix - fixes #664 by replacing __ with esc_html__.
= 3.1.6 - 2020-05-12 =
* Change log starts.

View File

@@ -0,0 +1,24 @@
<?php
// phpcs:ignoreFile
/**
* Class ActionScheduler_ActionClaim
*/
class ActionScheduler_ActionClaim {
private $id = '';
private $action_ids = array();
public function __construct( $id, array $action_ids ) {
$this->id = $id;
$this->action_ids = $action_ids;
}
public function get_id() {
return $this->id;
}
public function get_actions() {
return $this->action_ids;
}
}

View File

@@ -0,0 +1,358 @@
<?php
// phpcs:ignoreFile
/**
* Class ActionScheduler_ActionFactory
*/
class ActionScheduler_ActionFactory {
/**
* Return stored actions for given params.
*
* @param string $status The action's status in the data store.
* @param string $hook The hook to trigger when this action runs.
* @param array $args Args to pass to callbacks when the hook is triggered.
* @param ActionScheduler_Schedule $schedule The action's schedule.
* @param string $group A group to put the action in.
* phpcs:ignore Squiz.Commenting.FunctionComment.ExtraParamComment
* @param int $priority The action priority.
*
* @return ActionScheduler_Action An instance of the stored action.
*/
public function get_stored_action( $status, $hook, array $args = array(), ActionScheduler_Schedule $schedule = null, $group = '' ) {
// The 6th parameter ($priority) is not formally declared in the method signature to maintain compatibility with
// third-party subclasses created before this param was added.
$priority = func_num_args() >= 6 ? (int) func_get_arg( 5 ) : 10;
switch ( $status ) {
case ActionScheduler_Store::STATUS_PENDING:
$action_class = 'ActionScheduler_Action';
break;
case ActionScheduler_Store::STATUS_CANCELED:
$action_class = 'ActionScheduler_CanceledAction';
if ( ! is_null( $schedule ) && ! is_a( $schedule, 'ActionScheduler_CanceledSchedule' ) && ! is_a( $schedule, 'ActionScheduler_NullSchedule' ) ) {
$schedule = new ActionScheduler_CanceledSchedule( $schedule->get_date() );
}
break;
default:
$action_class = 'ActionScheduler_FinishedAction';
break;
}
$action_class = apply_filters( 'action_scheduler_stored_action_class', $action_class, $status, $hook, $args, $schedule, $group );
$action = new $action_class( $hook, $args, $schedule, $group );
$action->set_priority( $priority );
/**
* Allow 3rd party code to change the instantiated action for a given hook, args, schedule and group.
*
* @param ActionScheduler_Action $action The instantiated action.
* @param string $hook The instantiated action's hook.
* @param array $args The instantiated action's args.
* @param ActionScheduler_Schedule $schedule The instantiated action's schedule.
* @param string $group The instantiated action's group.
* @param int $priority The action priority.
*/
return apply_filters( 'action_scheduler_stored_action_instance', $action, $hook, $args, $schedule, $group, $priority );
}
/**
* Enqueue an action to run one time, as soon as possible (rather a specific scheduled time).
*
* This method creates a new action using the NullSchedule. In practice, this results in an action scheduled to
* execute "now". Therefore, it will generally run as soon as possible but is not prioritized ahead of other actions
* that are already past-due.
*
* @param string $hook The hook to trigger when this action runs.
* @param array $args Args to pass when the hook is triggered.
* @param string $group A group to put the action in.
*
* @return int The ID of the stored action.
*/
public function async( $hook, $args = array(), $group = '' ) {
return $this->async_unique( $hook, $args, $group, false );
}
/**
* Same as async, but also supports $unique param.
*
* @param string $hook The hook to trigger when this action runs.
* @param array $args Args to pass when the hook is triggered.
* @param string $group A group to put the action in.
* @param bool $unique Whether to ensure the action is unique.
*
* @return int The ID of the stored action.
*/
public function async_unique( $hook, $args = array(), $group = '', $unique = true ) {
$schedule = new ActionScheduler_NullSchedule();
$action = new ActionScheduler_Action( $hook, $args, $schedule, $group );
return $unique ? $this->store_unique_action( $action, $unique ) : $this->store( $action );
}
/**
* Create single action.
*
* @param string $hook The hook to trigger when this action runs.
* @param array $args Args to pass when the hook is triggered.
* @param int $when Unix timestamp when the action will run.
* @param string $group A group to put the action in.
*
* @return int The ID of the stored action.
*/
public function single( $hook, $args = array(), $when = null, $group = '' ) {
return $this->single_unique( $hook, $args, $when, $group, false );
}
/**
* Create single action only if there is no pending or running action with same name and params.
*
* @param string $hook The hook to trigger when this action runs.
* @param array $args Args to pass when the hook is triggered.
* @param int $when Unix timestamp when the action will run.
* @param string $group A group to put the action in.
* @param bool $unique Whether action scheduled should be unique.
*
* @return int The ID of the stored action.
*/
public function single_unique( $hook, $args = array(), $when = null, $group = '', $unique = true ) {
$date = as_get_datetime_object( $when );
$schedule = new ActionScheduler_SimpleSchedule( $date );
$action = new ActionScheduler_Action( $hook, $args, $schedule, $group );
return $unique ? $this->store_unique_action( $action ) : $this->store( $action );
}
/**
* Create the first instance of an action recurring on a given interval.
*
* @param string $hook The hook to trigger when this action runs.
* @param array $args Args to pass when the hook is triggered.
* @param int $first Unix timestamp for the first run.
* @param int $interval Seconds between runs.
* @param string $group A group to put the action in.
*
* @return int The ID of the stored action.
*/
public function recurring( $hook, $args = array(), $first = null, $interval = null, $group = '' ) {
return $this->recurring_unique( $hook, $args, $first, $interval, $group, false );
}
/**
* Create the first instance of an action recurring on a given interval only if there is no pending or running action with same name and params.
*
* @param string $hook The hook to trigger when this action runs.
* @param array $args Args to pass when the hook is triggered.
* @param int $first Unix timestamp for the first run.
* @param int $interval Seconds between runs.
* @param string $group A group to put the action in.
* @param bool $unique Whether action scheduled should be unique.
*
* @return int The ID of the stored action.
*/
public function recurring_unique( $hook, $args = array(), $first = null, $interval = null, $group = '', $unique = true ) {
if ( empty( $interval ) ) {
return $this->single_unique( $hook, $args, $first, $group, $unique );
}
$date = as_get_datetime_object( $first );
$schedule = new ActionScheduler_IntervalSchedule( $date, $interval );
$action = new ActionScheduler_Action( $hook, $args, $schedule, $group );
return $unique ? $this->store_unique_action( $action ) : $this->store( $action );
}
/**
* Create the first instance of an action recurring on a Cron schedule.
*
* @param string $hook The hook to trigger when this action runs.
* @param array $args Args to pass when the hook is triggered.
* @param int $base_timestamp The first instance of the action will be scheduled
* to run at a time calculated after this timestamp matching the cron
* expression. This can be used to delay the first instance of the action.
* @param int $schedule A cron definition string.
* @param string $group A group to put the action in.
*
* @return int The ID of the stored action.
*/
public function cron( $hook, $args = array(), $base_timestamp = null, $schedule = null, $group = '' ) {
return $this->cron_unique( $hook, $args, $base_timestamp, $schedule, $group, false );
}
/**
* Create the first instance of an action recurring on a Cron schedule only if there is no pending or running action with same name and params.
*
* @param string $hook The hook to trigger when this action runs.
* @param array $args Args to pass when the hook is triggered.
* @param int $base_timestamp The first instance of the action will be scheduled
* to run at a time calculated after this timestamp matching the cron
* expression. This can be used to delay the first instance of the action.
* @param int $schedule A cron definition string.
* @param string $group A group to put the action in.
* @param bool $unique Whether action scheduled should be unique.
*
* @return int The ID of the stored action.
**/
public function cron_unique( $hook, $args = array(), $base_timestamp = null, $schedule = null, $group = '', $unique = true ) {
if ( empty( $schedule ) ) {
return $this->single_unique( $hook, $args, $base_timestamp, $group, $unique );
}
$date = as_get_datetime_object( $base_timestamp );
$cron = CronExpression::factory( $schedule );
$schedule = new ActionScheduler_CronSchedule( $date, $cron );
$action = new ActionScheduler_Action( $hook, $args, $schedule, $group );
return $unique ? $this->store_unique_action( $action ) : $this->store( $action );
}
/**
* Create a successive instance of a recurring or cron action.
*
* Importantly, the action will be rescheduled to run based on the current date/time.
* That means when the action is scheduled to run in the past, the next scheduled date
* will be pushed forward. For example, if a recurring action set to run every hour
* was scheduled to run 5 seconds ago, it will be next scheduled for 1 hour in the
* future, which is 1 hour and 5 seconds from when it was last scheduled to run.
*
* Alternatively, if the action is scheduled to run in the future, and is run early,
* likely via manual intervention, then its schedule will change based on the time now.
* For example, if a recurring action set to run every day, and is run 12 hours early,
* it will run again in 24 hours, not 36 hours.
*
* This slippage is less of an issue with Cron actions, as the specific run time can
* be set for them to run, e.g. 1am each day. In those cases, and entire period would
* need to be missed before there was any change is scheduled, e.g. in the case of an
* action scheduled for 1am each day, the action would need to run an entire day late.
*
* @param ActionScheduler_Action $action The existing action.
*
* @return string The ID of the stored action
* @throws InvalidArgumentException If $action is not a recurring action.
*/
public function repeat( $action ) {
$schedule = $action->get_schedule();
$next = $schedule->get_next( as_get_datetime_object() );
if ( is_null( $next ) || ! $schedule->is_recurring() ) {
throw new InvalidArgumentException( __( 'Invalid action - must be a recurring action.', 'woocommerce' ) );
}
$schedule_class = get_class( $schedule );
$new_schedule = new $schedule( $next, $schedule->get_recurrence(), $schedule->get_first_date() );
$new_action = new ActionScheduler_Action( $action->get_hook(), $action->get_args(), $new_schedule, $action->get_group() );
$new_action->set_priority( $action->get_priority() );
return $this->store( $new_action );
}
/**
* Creates a scheduled action.
*
* This general purpose method can be used in place of specific methods such as async(),
* async_unique(), single() or single_unique(), etc.
*
* @internal Not intended for public use, should not be overriden by subclasses.
*
* @param array $options {
* Describes the action we wish to schedule.
*
* @type string $type Must be one of 'async', 'cron', 'recurring', or 'single'.
* @type string $hook The hook to be executed.
* @type array $arguments Arguments to be passed to the callback.
* @type string $group The action group.
* @type bool $unique If the action should be unique.
* @type int $when Timestamp. Indicates when the action, or first instance of the action in the case
* of recurring or cron actions, becomes due.
* @type int|string $pattern Recurrence pattern. This is either an interval in seconds for recurring actions
* or a cron expression for cron actions.
* @type int $priority Lower values means higher priority. Should be in the range 0-255.
* }
*
* @return int The action ID. Zero if there was an error scheduling the action.
*/
public function create( array $options = array() ) {
$defaults = array(
'type' => 'single',
'hook' => '',
'arguments' => array(),
'group' => '',
'unique' => false,
'when' => time(),
'pattern' => null,
'priority' => 10,
);
$options = array_merge( $defaults, $options );
// Cron/recurring actions without a pattern are treated as single actions (this gives calling code the ability
// to use functions like as_schedule_recurring_action() to schedule recurring as well as single actions).
if ( ( 'cron' === $options['type'] || 'recurring' === $options['type'] ) && empty( $options['pattern'] ) ) {
$options['type'] = 'single';
}
switch ( $options['type'] ) {
case 'async':
$schedule = new ActionScheduler_NullSchedule();
break;
case 'cron':
$date = as_get_datetime_object( $options['when'] );
$cron = CronExpression::factory( $options['pattern'] );
$schedule = new ActionScheduler_CronSchedule( $date, $cron );
break;
case 'recurring':
$date = as_get_datetime_object( $options['when'] );
$schedule = new ActionScheduler_IntervalSchedule( $date, $options['pattern'] );
break;
case 'single':
$date = as_get_datetime_object( $options['when'] );
$schedule = new ActionScheduler_SimpleSchedule( $date );
break;
default:
error_log( "Unknown action type '{$options['type']}' specified when trying to create an action for '{$options['hook']}'." );
return 0;
}
$action = new ActionScheduler_Action( $options['hook'], $options['arguments'], $schedule, $options['group'] );
$action->set_priority( $options['priority'] );
$action_id = 0;
try {
$action_id = $options['unique'] ? $this->store_unique_action( $action ) : $this->store( $action );
} catch ( Exception $e ) {
error_log(
sprintf(
/* translators: %1$s is the name of the hook to be enqueued, %2$s is the exception message. */
__( 'Caught exception while enqueuing action "%1$s": %2$s', 'woocommerce' ),
$options['hook'],
$e->getMessage()
)
);
}
return $action_id;
}
/**
* Save action to database.
*
* @param ActionScheduler_Action $action Action object to save.
*
* @return int The ID of the stored action
*/
protected function store( ActionScheduler_Action $action ) {
$store = ActionScheduler_Store::instance();
return $store->save_action( $action );
}
/**
* Store action if it's unique.
*
* @param ActionScheduler_Action $action Action object to store.
*
* @return int ID of the created action. Will be 0 if action was not created.
*/
protected function store_unique_action( ActionScheduler_Action $action ) {
$store = ActionScheduler_Store::instance();
return method_exists( $store, 'save_unique_action' ) ?
$store->save_unique_action( $action ) : $store->save_action( $action );
}
}

View File

@@ -0,0 +1,253 @@
<?php
// phpcs:ignoreFile
/**
* Class ActionScheduler_AdminView
* @codeCoverageIgnore
*/
class ActionScheduler_AdminView extends ActionScheduler_AdminView_Deprecated {
private static $admin_view = NULL;
private static $screen_id = 'tools_page_action-scheduler';
/** @var ActionScheduler_ListTable */
protected $list_table;
/**
* @return ActionScheduler_AdminView
* @codeCoverageIgnore
*/
public static function instance() {
if ( empty( self::$admin_view ) ) {
$class = apply_filters('action_scheduler_admin_view_class', 'ActionScheduler_AdminView');
self::$admin_view = new $class();
}
return self::$admin_view;
}
/**
* @codeCoverageIgnore
*/
public function init() {
if ( is_admin() && ( ! defined( 'DOING_AJAX' ) || false == DOING_AJAX ) ) {
if ( class_exists( 'WooCommerce' ) ) {
add_action( 'woocommerce_admin_status_content_action-scheduler', array( $this, 'render_admin_ui' ) );
add_action( 'woocommerce_system_status_report', array( $this, 'system_status_report' ) );
add_filter( 'woocommerce_admin_status_tabs', array( $this, 'register_system_status_tab' ) );
}
add_action( 'admin_menu', array( $this, 'register_menu' ) );
add_action( 'admin_notices', array( $this, 'maybe_check_pastdue_actions' ) );
add_action( 'current_screen', array( $this, 'add_help_tabs' ) );
}
}
public function system_status_report() {
$table = new ActionScheduler_wcSystemStatus( ActionScheduler::store() );
$table->render();
}
/**
* Registers action-scheduler into WooCommerce > System status.
*
* @param array $tabs An associative array of tab key => label.
* @return array $tabs An associative array of tab key => label, including Action Scheduler's tabs
*/
public function register_system_status_tab( array $tabs ) {
$tabs['action-scheduler'] = __( 'Scheduled Actions', 'woocommerce' );
return $tabs;
}
/**
* Include Action Scheduler's administration under the Tools menu.
*
* A menu under the Tools menu is important for backward compatibility (as that's
* where it started), and also provides more convenient access than the WooCommerce
* System Status page, and for sites where WooCommerce isn't active.
*/
public function register_menu() {
$hook_suffix = add_submenu_page(
'tools.php',
__( 'Scheduled Actions', 'woocommerce' ),
__( 'Scheduled Actions', 'woocommerce' ),
'manage_options',
'action-scheduler',
array( $this, 'render_admin_ui' )
);
add_action( 'load-' . $hook_suffix , array( $this, 'process_admin_ui' ) );
}
/**
* Triggers processing of any pending actions.
*/
public function process_admin_ui() {
$this->get_list_table();
}
/**
* Renders the Admin UI
*/
public function render_admin_ui() {
$table = $this->get_list_table();
$table->display_page();
}
/**
* Get the admin UI object and process any requested actions.
*
* @return ActionScheduler_ListTable
*/
protected function get_list_table() {
if ( null === $this->list_table ) {
$this->list_table = new ActionScheduler_ListTable( ActionScheduler::store(), ActionScheduler::logger(), ActionScheduler::runner() );
$this->list_table->process_actions();
}
return $this->list_table;
}
/**
* Action: admin_notices
*
* Maybe check past-due actions, and print notice.
*
* @uses $this->check_pastdue_actions()
*/
public function maybe_check_pastdue_actions() {
# Filter to prevent checking actions (ex: inappropriate user).
if ( ! apply_filters( 'action_scheduler_check_pastdue_actions', current_user_can( 'manage_options' ) ) ) {
return;
}
# Get last check transient.
$last_check = get_transient( 'action_scheduler_last_pastdue_actions_check' );
# If transient exists, we're within interval, so bail.
if ( ! empty( $last_check ) ) {
return;
}
# Perform the check.
$this->check_pastdue_actions();
}
/**
* Check past-due actions, and print notice.
*
* @todo update $link_url to "Past-due" filter when released (see issue #510, PR #511)
*/
protected function check_pastdue_actions() {
# Set thresholds.
$threshold_seconds = ( int ) apply_filters( 'action_scheduler_pastdue_actions_seconds', DAY_IN_SECONDS );
$threshhold_min = ( int ) apply_filters( 'action_scheduler_pastdue_actions_min', 1 );
// Set fallback value for past-due actions count.
$num_pastdue_actions = 0;
// Allow third-parties to preempt the default check logic.
$check = apply_filters( 'action_scheduler_pastdue_actions_check_pre', null );
// If no third-party preempted and there are no past-due actions, return early.
if ( ! is_null( $check ) ) {
return;
}
# Scheduled actions query arguments.
$query_args = array(
'date' => as_get_datetime_object( time() - $threshold_seconds ),
'status' => ActionScheduler_Store::STATUS_PENDING,
'per_page' => $threshhold_min,
);
# If no third-party preempted, run default check.
if ( is_null( $check ) ) {
$store = ActionScheduler_Store::instance();
$num_pastdue_actions = ( int ) $store->query_actions( $query_args, 'count' );
# Check if past-due actions count is greater than or equal to threshold.
$check = ( $num_pastdue_actions >= $threshhold_min );
$check = ( bool ) apply_filters( 'action_scheduler_pastdue_actions_check', $check, $num_pastdue_actions, $threshold_seconds, $threshhold_min );
}
# If check failed, set transient and abort.
if ( ! boolval( $check ) ) {
$interval = apply_filters( 'action_scheduler_pastdue_actions_check_interval', round( $threshold_seconds / 4 ), $threshold_seconds );
set_transient( 'action_scheduler_last_pastdue_actions_check', time(), $interval );
return;
}
$actions_url = add_query_arg( array(
'page' => 'action-scheduler',
'status' => 'past-due',
'order' => 'asc',
), admin_url( 'tools.php' ) );
# Print notice.
echo '<div class="notice notice-warning"><p>';
printf(
_n(
// translators: 1) is the number of affected actions, 2) is a link to an admin screen.
'<strong>Action Scheduler:</strong> %1$d <a href="%2$s">past-due action</a> found; something may be wrong. <a href="https://actionscheduler.org/faq/#my-site-has-past-due-actions-what-can-i-do" target="_blank">Read documentation &raquo;</a>',
'<strong>Action Scheduler:</strong> %1$d <a href="%2$s">past-due actions</a> found; something may be wrong. <a href="https://actionscheduler.org/faq/#my-site-has-past-due-actions-what-can-i-do" target="_blank">Read documentation &raquo;</a>',
$num_pastdue_actions,
'woocommerce'
),
$num_pastdue_actions,
esc_attr( esc_url( $actions_url ) )
);
echo '</p></div>';
# Facilitate third-parties to evaluate and print notices.
do_action( 'action_scheduler_pastdue_actions_extra_notices', $query_args );
}
/**
* Provide more information about the screen and its data in the help tab.
*/
public function add_help_tabs() {
$screen = get_current_screen();
if ( ! $screen || self::$screen_id != $screen->id ) {
return;
}
$as_version = ActionScheduler_Versions::instance()->latest_version();
$screen->add_help_tab(
array(
'id' => 'action_scheduler_about',
'title' => __( 'About', 'woocommerce' ),
'content' =>
'<h2>' . sprintf( __( 'About Action Scheduler %s', 'woocommerce' ), $as_version ) . '</h2>' .
'<p>' .
__( 'Action Scheduler is a scalable, traceable job queue for background processing large sets of actions. Action Scheduler works by triggering an action hook to run at some time in the future. Scheduled actions can also be scheduled to run on a recurring schedule.', 'woocommerce' ) .
'</p>',
)
);
$screen->add_help_tab(
array(
'id' => 'action_scheduler_columns',
'title' => __( 'Columns', 'woocommerce' ),
'content' =>
'<h2>' . __( 'Scheduled Action Columns', 'woocommerce' ) . '</h2>' .
'<ul>' .
sprintf( '<li><strong>%1$s</strong>: %2$s</li>', __( 'Hook', 'woocommerce' ), __( 'Name of the action hook that will be triggered.', 'woocommerce' ) ) .
sprintf( '<li><strong>%1$s</strong>: %2$s</li>', __( 'Status', 'woocommerce' ), __( 'Action statuses are Pending, Complete, Canceled, Failed', 'woocommerce' ) ) .
sprintf( '<li><strong>%1$s</strong>: %2$s</li>', __( 'Arguments', 'woocommerce' ), __( 'Optional data array passed to the action hook.', 'woocommerce' ) ) .
sprintf( '<li><strong>%1$s</strong>: %2$s</li>', __( 'Group', 'woocommerce' ), __( 'Optional action group.', 'woocommerce' ) ) .
sprintf( '<li><strong>%1$s</strong>: %2$s</li>', __( 'Recurrence', 'woocommerce' ), __( 'The action\'s schedule frequency.', 'woocommerce' ) ) .
sprintf( '<li><strong>%1$s</strong>: %2$s</li>', __( 'Scheduled', 'woocommerce' ), __( 'The date/time the action is/was scheduled to run.', 'woocommerce' ) ) .
sprintf( '<li><strong>%1$s</strong>: %2$s</li>', __( 'Log', 'woocommerce' ), __( 'Activity log for the action.', 'woocommerce' ) ) .
'</ul>',
)
);
}
}

View File

@@ -0,0 +1,98 @@
<?php
// phpcs:ignoreFile
/**
* ActionScheduler_AsyncRequest_QueueRunner
*/
defined( 'ABSPATH' ) || exit;
/**
* ActionScheduler_AsyncRequest_QueueRunner class.
*/
class ActionScheduler_AsyncRequest_QueueRunner extends WP_Async_Request {
/**
* Data store for querying actions
*
* @var ActionScheduler_Store
* @access protected
*/
protected $store;
/**
* Prefix for ajax hooks
*
* @var string
* @access protected
*/
protected $prefix = 'as';
/**
* Action for ajax hooks
*
* @var string
* @access protected
*/
protected $action = 'async_request_queue_runner';
/**
* Initiate new async request
*/
public function __construct( ActionScheduler_Store $store ) {
parent::__construct();
$this->store = $store;
}
/**
* Handle async requests
*
* Run a queue, and maybe dispatch another async request to run another queue
* if there are still pending actions after completing a queue in this request.
*/
protected function handle() {
do_action( 'action_scheduler_run_queue', 'Async Request' ); // run a queue in the same way as WP Cron, but declare the Async Request context
$sleep_seconds = $this->get_sleep_seconds();
if ( $sleep_seconds ) {
sleep( $sleep_seconds );
}
$this->maybe_dispatch();
}
/**
* If the async request runner is needed and allowed to run, dispatch a request.
*/
public function maybe_dispatch() {
if ( ! $this->allow() ) {
return;
}
$this->dispatch();
ActionScheduler_QueueRunner::instance()->unhook_dispatch_async_request();
}
/**
* Only allow async requests when needed.
*
* Also allow 3rd party code to disable running actions via async requests.
*/
protected function allow() {
if ( ! has_action( 'action_scheduler_run_queue' ) || ActionScheduler::runner()->has_maximum_concurrent_batches() || ! $this->store->has_pending_actions_due() ) {
$allow = false;
} else {
$allow = true;
}
return apply_filters( 'action_scheduler_allow_async_request_runner', $allow );
}
/**
* Chaining async requests can crash MySQL. A brief sleep call in PHP prevents that.
*/
protected function get_sleep_seconds() {
return apply_filters( 'action_scheduler_async_request_sleep_seconds', 5, $this );
}
}

View File

@@ -0,0 +1,106 @@
<?php
// phpcs:ignoreFile
/**
* Class ActionScheduler_Compatibility
*/
class ActionScheduler_Compatibility {
/**
* Converts a shorthand byte value to an integer byte value.
*
* Wrapper for wp_convert_hr_to_bytes(), moved to load.php in WordPress 4.6 from media.php
*
* @link https://secure.php.net/manual/en/function.ini-get.php
* @link https://secure.php.net/manual/en/faq.using.php#faq.using.shorthandbytes
*
* @param string $value A (PHP ini) byte value, either shorthand or ordinary.
* @return int An integer byte value.
*/
public static function convert_hr_to_bytes( $value ) {
if ( function_exists( 'wp_convert_hr_to_bytes' ) ) {
return wp_convert_hr_to_bytes( $value );
}
$value = strtolower( trim( $value ) );
$bytes = (int) $value;
if ( false !== strpos( $value, 'g' ) ) {
$bytes *= GB_IN_BYTES;
} elseif ( false !== strpos( $value, 'm' ) ) {
$bytes *= MB_IN_BYTES;
} elseif ( false !== strpos( $value, 'k' ) ) {
$bytes *= KB_IN_BYTES;
}
// Deal with large (float) values which run into the maximum integer size.
return min( $bytes, PHP_INT_MAX );
}
/**
* Attempts to raise the PHP memory limit for memory intensive processes.
*
* Only allows raising the existing limit and prevents lowering it.
*
* Wrapper for wp_raise_memory_limit(), added in WordPress v4.6.0
*
* @return bool|int|string The limit that was set or false on failure.
*/
public static function raise_memory_limit() {
if ( function_exists( 'wp_raise_memory_limit' ) ) {
return wp_raise_memory_limit( 'admin' );
}
$current_limit = @ini_get( 'memory_limit' );
$current_limit_int = self::convert_hr_to_bytes( $current_limit );
if ( -1 === $current_limit_int ) {
return false;
}
$wp_max_limit = WP_MAX_MEMORY_LIMIT;
$wp_max_limit_int = self::convert_hr_to_bytes( $wp_max_limit );
$filtered_limit = apply_filters( 'admin_memory_limit', $wp_max_limit );
$filtered_limit_int = self::convert_hr_to_bytes( $filtered_limit );
if ( -1 === $filtered_limit_int || ( $filtered_limit_int > $wp_max_limit_int && $filtered_limit_int > $current_limit_int ) ) {
if ( false !== @ini_set( 'memory_limit', $filtered_limit ) ) {
return $filtered_limit;
} else {
return false;
}
} elseif ( -1 === $wp_max_limit_int || $wp_max_limit_int > $current_limit_int ) {
if ( false !== @ini_set( 'memory_limit', $wp_max_limit ) ) {
return $wp_max_limit;
} else {
return false;
}
}
return false;
}
/**
* Attempts to raise the PHP timeout for time intensive processes.
*
* Only allows raising the existing limit and prevents lowering it. Wrapper for wc_set_time_limit(), when available.
*
* @param int $limit The time limit in seconds.
*/
public static function raise_time_limit( $limit = 0 ) {
$limit = (int) $limit;
$max_execution_time = (int) ini_get( 'max_execution_time' );
// If the max execution time is already set to zero (unlimited), there is no reason to make a further change.
if ( 0 === $max_execution_time ) {
return;
}
// Whichever of $max_execution_time or $limit is higher is the amount by which we raise the time limit.
$raise_by = 0 === $limit || $limit > $max_execution_time ? $limit : $max_execution_time;
if ( function_exists( 'wc_set_time_limit' ) ) {
wc_set_time_limit( $raise_by );
} elseif ( function_exists( 'set_time_limit' ) && false === strpos( ini_get( 'disable_functions' ), 'set_time_limit' ) && ! ini_get( 'safe_mode' ) ) { // phpcs:ignore PHPCompatibility.IniDirectives.RemovedIniDirectives.safe_modeDeprecatedRemoved
@set_time_limit( $raise_by ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
}
}
}

View File

@@ -0,0 +1,188 @@
<?php
// phpcs:ignoreFile
use Action_Scheduler\Migration\Controller;
/**
* Class ActionScheduler_DataController
*
* The main plugin/initialization class for the data stores.
*
* Responsible for hooking everything up with WordPress.
*
* @package Action_Scheduler
*
* @since 3.0.0
*/
class ActionScheduler_DataController {
/** Action data store class name. */
const DATASTORE_CLASS = 'ActionScheduler_DBStore';
/** Logger data store class name. */
const LOGGER_CLASS = 'ActionScheduler_DBLogger';
/** Migration status option name. */
const STATUS_FLAG = 'action_scheduler_migration_status';
/** Migration status option value. */
const STATUS_COMPLETE = 'complete';
/** Migration minimum required PHP version. */
const MIN_PHP_VERSION = '5.5';
/** @var ActionScheduler_DataController */
private static $instance;
/** @var int */
private static $sleep_time = 0;
/** @var int */
private static $free_ticks = 50;
/**
* Get a flag indicating whether the migration environment dependencies are met.
*
* @return bool
*/
public static function dependencies_met() {
$php_support = version_compare( PHP_VERSION, self::MIN_PHP_VERSION, '>=' );
return $php_support && apply_filters( 'action_scheduler_migration_dependencies_met', true );
}
/**
* Get a flag indicating whether the migration is complete.
*
* @return bool Whether the flag has been set marking the migration as complete
*/
public static function is_migration_complete() {
return get_option( self::STATUS_FLAG ) === self::STATUS_COMPLETE;
}
/**
* Mark the migration as complete.
*/
public static function mark_migration_complete() {
update_option( self::STATUS_FLAG, self::STATUS_COMPLETE );
}
/**
* Unmark migration when a plugin is de-activated. Will not work in case of silent activation, for example in an update.
* We do this to mitigate the bug of lost actions which happens if there was an AS 2.x to AS 3.x migration in the past, but that plugin is now
* deactivated and the site was running on AS 2.x again.
*/
public static function mark_migration_incomplete() {
delete_option( self::STATUS_FLAG );
}
/**
* Set the action store class name.
*
* @param string $class Classname of the store class.
*
* @return string
*/
public static function set_store_class( $class ) {
return self::DATASTORE_CLASS;
}
/**
* Set the action logger class name.
*
* @param string $class Classname of the logger class.
*
* @return string
*/
public static function set_logger_class( $class ) {
return self::LOGGER_CLASS;
}
/**
* Set the sleep time in seconds.
*
* @param integer $sleep_time The number of seconds to pause before resuming operation.
*/
public static function set_sleep_time( $sleep_time ) {
self::$sleep_time = (int) $sleep_time;
}
/**
* Set the tick count required for freeing memory.
*
* @param integer $free_ticks The number of ticks to free memory on.
*/
public static function set_free_ticks( $free_ticks ) {
self::$free_ticks = (int) $free_ticks;
}
/**
* Free memory if conditions are met.
*
* @param int $ticks Current tick count.
*/
public static function maybe_free_memory( $ticks ) {
if ( self::$free_ticks && 0 === $ticks % self::$free_ticks ) {
self::free_memory();
}
}
/**
* Reduce memory footprint by clearing the database query and object caches.
*/
public static function free_memory() {
if ( 0 < self::$sleep_time ) {
/* translators: %d: amount of time */
\WP_CLI::warning( sprintf( _n( 'Stopped the insanity for %d second', 'Stopped the insanity for %d seconds', self::$sleep_time, 'woocommerce' ), self::$sleep_time ) );
sleep( self::$sleep_time );
}
\WP_CLI::warning( __( 'Attempting to reduce used memory...', 'woocommerce' ) );
/**
* @var $wpdb \wpdb
* @var $wp_object_cache \WP_Object_Cache
*/
global $wpdb, $wp_object_cache;
$wpdb->queries = array();
if ( ! is_a( $wp_object_cache, 'WP_Object_Cache' ) ) {
return;
}
$wp_object_cache->group_ops = array();
$wp_object_cache->stats = array();
$wp_object_cache->memcache_debug = array();
$wp_object_cache->cache = array();
if ( is_callable( array( $wp_object_cache, '__remoteset' ) ) ) {
call_user_func( array( $wp_object_cache, '__remoteset' ) ); // important
}
}
/**
* Connect to table datastores if migration is complete.
* Otherwise, proceed with the migration if the dependencies have been met.
*/
public static function init() {
if ( self::is_migration_complete() ) {
add_filter( 'action_scheduler_store_class', array( 'ActionScheduler_DataController', 'set_store_class' ), 100 );
add_filter( 'action_scheduler_logger_class', array( 'ActionScheduler_DataController', 'set_logger_class' ), 100 );
add_action( 'deactivate_plugin', array( 'ActionScheduler_DataController', 'mark_migration_incomplete' ) );
} elseif ( self::dependencies_met() ) {
Controller::init();
}
add_action( 'action_scheduler/progress_tick', array( 'ActionScheduler_DataController', 'maybe_free_memory' ) );
}
/**
* Singleton factory.
*/
public static function instance() {
if ( ! isset( self::$instance ) ) {
self::$instance = new static();
}
return self::$instance;
}
}

View File

@@ -0,0 +1,80 @@
<?php
// phpcs:ignoreFile
/**
* ActionScheduler DateTime class.
*
* This is a custom extension to DateTime that
*/
class ActionScheduler_DateTime extends DateTime {
/**
* UTC offset.
*
* Only used when a timezone is not set. When a timezone string is
* used, this will be set to 0.
*
* @var int
*/
protected $utcOffset = 0;
/**
* Get the unix timestamp of the current object.
*
* Missing in PHP 5.2 so just here so it can be supported consistently.
*
* @return int
*/
#[\ReturnTypeWillChange]
public function getTimestamp() {
return method_exists( 'DateTime', 'getTimestamp' ) ? parent::getTimestamp() : $this->format( 'U' );
}
/**
* Set the UTC offset.
*
* This represents a fixed offset instead of a timezone setting.
*
* @param $offset
*/
public function setUtcOffset( $offset ) {
$this->utcOffset = intval( $offset );
}
/**
* Returns the timezone offset.
*
* @return int
* @link http://php.net/manual/en/datetime.getoffset.php
*/
#[\ReturnTypeWillChange]
public function getOffset() {
return $this->utcOffset ? $this->utcOffset : parent::getOffset();
}
/**
* Set the TimeZone associated with the DateTime
*
* @param DateTimeZone $timezone
*
* @return static
* @link http://php.net/manual/en/datetime.settimezone.php
*/
#[\ReturnTypeWillChange]
public function setTimezone( $timezone ) {
$this->utcOffset = 0;
parent::setTimezone( $timezone );
return $this;
}
/**
* Get the timestamp with the WordPress timezone offset added or subtracted.
*
* @since 3.0.0
* @return int
*/
public function getOffsetTimestamp() {
return $this->getTimestamp() + $this->getOffset();
}
}

View File

@@ -0,0 +1,12 @@
<?php
// phpcs:ignoreFile
/**
* ActionScheduler Exception Interface.
*
* Facilitates catching Exceptions unique to Action Scheduler.
*
* @package ActionScheduler
* @since 2.1.0
*/
interface ActionScheduler_Exception {}

View File

@@ -0,0 +1,56 @@
<?php
// phpcs:ignoreFile
/**
* Class ActionScheduler_FatalErrorMonitor
*/
class ActionScheduler_FatalErrorMonitor {
/** @var ActionScheduler_ActionClaim */
private $claim = NULL;
/** @var ActionScheduler_Store */
private $store = NULL;
private $action_id = 0;
public function __construct( ActionScheduler_Store $store ) {
$this->store = $store;
}
public function attach( ActionScheduler_ActionClaim $claim ) {
$this->claim = $claim;
add_action( 'shutdown', array( $this, 'handle_unexpected_shutdown' ) );
add_action( 'action_scheduler_before_execute', array( $this, 'track_current_action' ), 0, 1 );
add_action( 'action_scheduler_after_execute', array( $this, 'untrack_action' ), 0, 0 );
add_action( 'action_scheduler_execution_ignored', array( $this, 'untrack_action' ), 0, 0 );
add_action( 'action_scheduler_failed_execution', array( $this, 'untrack_action' ), 0, 0 );
}
public function detach() {
$this->claim = NULL;
$this->untrack_action();
remove_action( 'shutdown', array( $this, 'handle_unexpected_shutdown' ) );
remove_action( 'action_scheduler_before_execute', array( $this, 'track_current_action' ), 0 );
remove_action( 'action_scheduler_after_execute', array( $this, 'untrack_action' ), 0 );
remove_action( 'action_scheduler_execution_ignored', array( $this, 'untrack_action' ), 0 );
remove_action( 'action_scheduler_failed_execution', array( $this, 'untrack_action' ), 0 );
}
public function track_current_action( $action_id ) {
$this->action_id = $action_id;
}
public function untrack_action() {
$this->action_id = 0;
}
public function handle_unexpected_shutdown() {
if ( $error = error_get_last() ) {
if ( in_array( $error['type'], array( E_ERROR, E_PARSE, E_COMPILE_ERROR, E_USER_ERROR, E_RECOVERABLE_ERROR ) ) ) {
if ( !empty($this->action_id) ) {
$this->store->mark_failure( $this->action_id );
do_action( 'action_scheduler_unexpected_shutdown', $this->action_id, $error );
}
}
$this->store->release_claim( $this->claim );
}
}
}

View File

@@ -0,0 +1,48 @@
<?php
// phpcs:ignoreFile
/**
* InvalidAction Exception.
*
* Used for identifying actions that are invalid in some way.
*
* @package ActionScheduler
*/
class ActionScheduler_InvalidActionException extends \InvalidArgumentException implements ActionScheduler_Exception {
/**
* Create a new exception when the action's schedule cannot be fetched.
*
* @param string $action_id The action ID with bad args.
* @return static
*/
public static function from_schedule( $action_id, $schedule ) {
$message = sprintf(
/* translators: 1: action ID 2: schedule */
__( 'Action [%1$s] has an invalid schedule: %2$s', 'woocommerce' ),
$action_id,
var_export( $schedule, true )
);
return new static( $message );
}
/**
* Create a new exception when the action's args cannot be decoded to an array.
*
* @author Jeremy Pry
*
* @param string $action_id The action ID with bad args.
* @return static
*/
public static function from_decoding_args( $action_id, $args = array() ) {
$message = sprintf(
/* translators: 1: action ID 2: arguments */
__( 'Action [%1$s] has invalid arguments. It cannot be JSON decoded to an array. $args = %2$s', 'woocommerce' ),
$action_id,
var_export( $args, true )
);
return new static( $message );
}
}

View File

@@ -0,0 +1,671 @@
<?php
// phpcs:ignoreFile
/**
* Implements the admin view of the actions.
* @codeCoverageIgnore
*/
class ActionScheduler_ListTable extends ActionScheduler_Abstract_ListTable {
/**
* The package name.
*
* @var string
*/
protected $package = 'action-scheduler';
/**
* Columns to show (name => label).
*
* @var array
*/
protected $columns = array();
/**
* Actions (name => label).
*
* @var array
*/
protected $row_actions = array();
/**
* The active data stores
*
* @var ActionScheduler_Store
*/
protected $store;
/**
* A logger to use for getting action logs to display
*
* @var ActionScheduler_Logger
*/
protected $logger;
/**
* A ActionScheduler_QueueRunner runner instance (or child class)
*
* @var ActionScheduler_QueueRunner
*/
protected $runner;
/**
* Bulk actions. The key of the array is the method name of the implementation:
*
* bulk_<key>(array $ids, string $sql_in).
*
* See the comments in the parent class for further details
*
* @var array
*/
protected $bulk_actions = array();
/**
* Flag variable to render our notifications, if any, once.
*
* @var bool
*/
protected static $did_notification = false;
/**
* Array of seconds for common time periods, like week or month, alongside an internationalised string representation, i.e. "Day" or "Days"
*
* @var array
*/
private static $time_periods;
/**
* Sets the current data store object into `store->action` and initialises the object.
*
* @param ActionScheduler_Store $store
* @param ActionScheduler_Logger $logger
* @param ActionScheduler_QueueRunner $runner
*/
public function __construct( ActionScheduler_Store $store, ActionScheduler_Logger $logger, ActionScheduler_QueueRunner $runner ) {
$this->store = $store;
$this->logger = $logger;
$this->runner = $runner;
$this->table_header = __( 'Scheduled Actions', 'woocommerce' );
$this->bulk_actions = array(
'delete' => __( 'Delete', 'woocommerce' ),
);
$this->columns = array(
'hook' => __( 'Hook', 'woocommerce' ),
'status' => __( 'Status', 'woocommerce' ),
'args' => __( 'Arguments', 'woocommerce' ),
'group' => __( 'Group', 'woocommerce' ),
'recurrence' => __( 'Recurrence', 'woocommerce' ),
'schedule' => __( 'Scheduled Date', 'woocommerce' ),
'log_entries' => __( 'Log', 'woocommerce' ),
);
$this->sort_by = array(
'schedule',
'hook',
'group',
);
$this->search_by = array(
'hook',
'args',
'claim_id',
);
$request_status = $this->get_request_status();
if ( empty( $request_status ) ) {
$this->sort_by[] = 'status';
} elseif ( in_array( $request_status, array( 'in-progress', 'failed' ) ) ) {
$this->columns += array( 'claim_id' => __( 'Claim ID', 'woocommerce' ) );
$this->sort_by[] = 'claim_id';
}
$this->row_actions = array(
'hook' => array(
'run' => array(
'name' => __( 'Run', 'woocommerce' ),
'desc' => __( 'Process the action now as if it were run as part of a queue', 'woocommerce' ),
),
'cancel' => array(
'name' => __( 'Cancel', 'woocommerce' ),
'desc' => __( 'Cancel the action now to avoid it being run in future', 'woocommerce' ),
'class' => 'cancel trash',
),
),
);
self::$time_periods = array(
array(
'seconds' => YEAR_IN_SECONDS,
/* translators: %s: amount of time */
'names' => _n_noop( '%s year', '%s years', 'woocommerce' ),
),
array(
'seconds' => MONTH_IN_SECONDS,
/* translators: %s: amount of time */
'names' => _n_noop( '%s month', '%s months', 'woocommerce' ),
),
array(
'seconds' => WEEK_IN_SECONDS,
/* translators: %s: amount of time */
'names' => _n_noop( '%s week', '%s weeks', 'woocommerce' ),
),
array(
'seconds' => DAY_IN_SECONDS,
/* translators: %s: amount of time */
'names' => _n_noop( '%s day', '%s days', 'woocommerce' ),
),
array(
'seconds' => HOUR_IN_SECONDS,
/* translators: %s: amount of time */
'names' => _n_noop( '%s hour', '%s hours', 'woocommerce' ),
),
array(
'seconds' => MINUTE_IN_SECONDS,
/* translators: %s: amount of time */
'names' => _n_noop( '%s minute', '%s minutes', 'woocommerce' ),
),
array(
'seconds' => 1,
/* translators: %s: amount of time */
'names' => _n_noop( '%s second', '%s seconds', 'woocommerce' ),
),
);
parent::__construct(
array(
'singular' => 'action-scheduler',
'plural' => 'action-scheduler',
'ajax' => false,
)
);
add_screen_option(
'per_page',
array(
'default' => $this->items_per_page,
)
);
add_filter( 'set_screen_option_' . $this->get_per_page_option_name(), array( $this, 'set_items_per_page_option' ), 10, 3 );
set_screen_options();
}
/**
* Handles setting the items_per_page option for this screen.
*
* @param mixed $status Default false (to skip saving the current option).
* @param string $option Screen option name.
* @param int $value Screen option value.
* @return int
*/
public function set_items_per_page_option( $status, $option, $value ) {
return $value;
}
/**
* Convert an interval of seconds into a two part human friendly string.
*
* The WordPress human_time_diff() function only calculates the time difference to one degree, meaning
* even if an action is 1 day and 11 hours away, it will display "1 day". This function goes one step
* further to display two degrees of accuracy.
*
* Inspired by the Crontrol::interval() function by Edward Dale: https://wordpress.org/plugins/wp-crontrol/
*
* @param int $interval A interval in seconds.
* @param int $periods_to_include Depth of time periods to include, e.g. for an interval of 70, and $periods_to_include of 2, both minutes and seconds would be included. With a value of 1, only minutes would be included.
* @return string A human friendly string representation of the interval.
*/
private static function human_interval( $interval, $periods_to_include = 2 ) {
if ( $interval <= 0 ) {
return __( 'Now!', 'woocommerce' );
}
$output = '';
for ( $time_period_index = 0, $periods_included = 0, $seconds_remaining = $interval; $time_period_index < count( self::$time_periods ) && $seconds_remaining > 0 && $periods_included < $periods_to_include; $time_period_index++ ) {
$periods_in_interval = floor( $seconds_remaining / self::$time_periods[ $time_period_index ]['seconds'] );
if ( $periods_in_interval > 0 ) {
if ( ! empty( $output ) ) {
$output .= ' ';
}
$output .= sprintf( _n( self::$time_periods[ $time_period_index ]['names'][0], self::$time_periods[ $time_period_index ]['names'][1], $periods_in_interval, 'woocommerce' ), $periods_in_interval );
$seconds_remaining -= $periods_in_interval * self::$time_periods[ $time_period_index ]['seconds'];
$periods_included++;
}
}
return $output;
}
/**
* Returns the recurrence of an action or 'Non-repeating'. The output is human readable.
*
* @param ActionScheduler_Action $action
*
* @return string
*/
protected function get_recurrence( $action ) {
$schedule = $action->get_schedule();
if ( $schedule->is_recurring() && method_exists( $schedule, 'get_recurrence' ) ) {
$recurrence = $schedule->get_recurrence();
if ( is_numeric( $recurrence ) ) {
/* translators: %s: time interval */
return sprintf( __( 'Every %s', 'woocommerce' ), self::human_interval( $recurrence ) );
} else {
return $recurrence;
}
}
return __( 'Non-repeating', 'woocommerce' );
}
/**
* Serializes the argument of an action to render it in a human friendly format.
*
* @param array $row The array representation of the current row of the table
*
* @return string
*/
public function column_args( array $row ) {
if ( empty( $row['args'] ) ) {
return apply_filters( 'action_scheduler_list_table_column_args', '', $row );
}
$row_html = '<ul>';
foreach ( $row['args'] as $key => $value ) {
$row_html .= sprintf( '<li><code>%s => %s</code></li>', esc_html( var_export( $key, true ) ), esc_html( var_export( $value, true ) ) );
}
$row_html .= '</ul>';
return apply_filters( 'action_scheduler_list_table_column_args', $row_html, $row );
}
/**
* Prints the logs entries inline. We do so to avoid loading Javascript and other hacks to show it in a modal.
*
* @param array $row Action array.
* @return string
*/
public function column_log_entries( array $row ) {
$log_entries_html = '<ol>';
$timezone = new DateTimezone( 'UTC' );
foreach ( $row['log_entries'] as $log_entry ) {
$log_entries_html .= $this->get_log_entry_html( $log_entry, $timezone );
}
$log_entries_html .= '</ol>';
return $log_entries_html;
}
/**
* Prints the logs entries inline. We do so to avoid loading Javascript and other hacks to show it in a modal.
*
* @param ActionScheduler_LogEntry $log_entry
* @param DateTimezone $timezone
* @return string
*/
protected function get_log_entry_html( ActionScheduler_LogEntry $log_entry, DateTimezone $timezone ) {
$date = $log_entry->get_date();
$date->setTimezone( $timezone );
return sprintf( '<li><strong>%s</strong><br/>%s</li>', esc_html( $date->format( 'Y-m-d H:i:s O' ) ), esc_html( $log_entry->get_message() ) );
}
/**
* Only display row actions for pending actions.
*
* @param array $row Row to render
* @param string $column_name Current row
*
* @return string
*/
protected function maybe_render_actions( $row, $column_name ) {
if ( 'pending' === strtolower( $row[ 'status_name' ] ) ) {
return parent::maybe_render_actions( $row, $column_name );
}
return '';
}
/**
* Renders admin notifications
*
* Notifications:
* 1. When the maximum number of tasks are being executed simultaneously.
* 2. Notifications when a task is manually executed.
* 3. Tables are missing.
*/
public function display_admin_notices() {
global $wpdb;
if ( ( is_a( $this->store, 'ActionScheduler_HybridStore' ) || is_a( $this->store, 'ActionScheduler_DBStore' ) ) && apply_filters( 'action_scheduler_enable_recreate_data_store', true ) ) {
$table_list = array(
'actionscheduler_actions',
'actionscheduler_logs',
'actionscheduler_groups',
'actionscheduler_claims',
);
$found_tables = $wpdb->get_col( "SHOW TABLES LIKE '{$wpdb->prefix}actionscheduler%'" ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
foreach ( $table_list as $table_name ) {
if ( ! in_array( $wpdb->prefix . $table_name, $found_tables ) ) {
$this->admin_notices[] = array(
'class' => 'error',
'message' => __( 'It appears one or more database tables were missing. Attempting to re-create the missing table(s).' , 'woocommerce' ),
);
$this->recreate_tables();
parent::display_admin_notices();
return;
}
}
}
if ( $this->runner->has_maximum_concurrent_batches() ) {
$claim_count = $this->store->get_claim_count();
$this->admin_notices[] = array(
'class' => 'updated',
'message' => sprintf(
/* translators: %s: amount of claims */
_n(
'Maximum simultaneous queues already in progress (%s queue). No additional queues will begin processing until the current queues are complete.',
'Maximum simultaneous queues already in progress (%s queues). No additional queues will begin processing until the current queues are complete.',
$claim_count,
'woocommerce'
),
$claim_count
),
);
} elseif ( $this->store->has_pending_actions_due() ) {
$async_request_lock_expiration = ActionScheduler::lock()->get_expiration( 'async-request-runner' );
// No lock set or lock expired
if ( false === $async_request_lock_expiration || $async_request_lock_expiration < time() ) {
$in_progress_url = add_query_arg( 'status', 'in-progress', remove_query_arg( 'status' ) );
/* translators: %s: process URL */
$async_request_message = sprintf( __( 'A new queue has begun processing. <a href="%s">View actions in-progress &raquo;</a>', 'woocommerce' ), esc_url( $in_progress_url ) );
} else {
/* translators: %d: seconds */
$async_request_message = sprintf( __( 'The next queue will begin processing in approximately %d seconds.', 'woocommerce' ), $async_request_lock_expiration - time() );
}
$this->admin_notices[] = array(
'class' => 'notice notice-info',
'message' => $async_request_message,
);
}
$notification = get_transient( 'action_scheduler_admin_notice' );
if ( is_array( $notification ) ) {
delete_transient( 'action_scheduler_admin_notice' );
$action = $this->store->fetch_action( $notification['action_id'] );
$action_hook_html = '<strong><code>' . $action->get_hook() . '</code></strong>';
if ( 1 == $notification['success'] ) {
$class = 'updated';
switch ( $notification['row_action_type'] ) {
case 'run' :
/* translators: %s: action HTML */
$action_message_html = sprintf( __( 'Successfully executed action: %s', 'woocommerce' ), $action_hook_html );
break;
case 'cancel' :
/* translators: %s: action HTML */
$action_message_html = sprintf( __( 'Successfully canceled action: %s', 'woocommerce' ), $action_hook_html );
break;
default :
/* translators: %s: action HTML */
$action_message_html = sprintf( __( 'Successfully processed change for action: %s', 'woocommerce' ), $action_hook_html );
break;
}
} else {
$class = 'error';
/* translators: 1: action HTML 2: action ID 3: error message */
$action_message_html = sprintf( __( 'Could not process change for action: "%1$s" (ID: %2$d). Error: %3$s', 'woocommerce' ), $action_hook_html, esc_html( $notification['action_id'] ), esc_html( $notification['error_message'] ) );
}
$action_message_html = apply_filters( 'action_scheduler_admin_notice_html', $action_message_html, $action, $notification );
$this->admin_notices[] = array(
'class' => $class,
'message' => $action_message_html,
);
}
parent::display_admin_notices();
}
/**
* Prints the scheduled date in a human friendly format.
*
* @param array $row The array representation of the current row of the table
*
* @return string
*/
public function column_schedule( $row ) {
return $this->get_schedule_display_string( $row['schedule'] );
}
/**
* Get the scheduled date in a human friendly format.
*
* @param ActionScheduler_Schedule $schedule
* @return string
*/
protected function get_schedule_display_string( ActionScheduler_Schedule $schedule ) {
$schedule_display_string = '';
if ( is_a( $schedule, 'ActionScheduler_NullSchedule' ) ) {
return __( 'async', 'woocommerce' );
}
if ( ! method_exists( $schedule, 'get_date' ) || ! $schedule->get_date() ) {
return '0000-00-00 00:00:00';
}
$next_timestamp = $schedule->get_date()->getTimestamp();
$schedule_display_string .= $schedule->get_date()->format( 'Y-m-d H:i:s O' );
$schedule_display_string .= '<br/>';
if ( gmdate( 'U' ) > $next_timestamp ) {
/* translators: %s: date interval */
$schedule_display_string .= sprintf( __( ' (%s ago)', 'woocommerce' ), self::human_interval( gmdate( 'U' ) - $next_timestamp ) );
} else {
/* translators: %s: date interval */
$schedule_display_string .= sprintf( __( ' (%s)', 'woocommerce' ), self::human_interval( $next_timestamp - gmdate( 'U' ) ) );
}
return $schedule_display_string;
}
/**
* Bulk delete
*
* Deletes actions based on their ID. This is the handler for the bulk delete. It assumes the data
* properly validated by the callee and it will delete the actions without any extra validation.
*
* @param array $ids
* @param string $ids_sql Inherited and unused
*/
protected function bulk_delete( array $ids, $ids_sql ) {
foreach ( $ids as $id ) {
try {
$this->store->delete_action( $id );
} catch ( Exception $e ) {
// A possible reason for an exception would include a scenario where the same action is deleted by a
// concurrent request.
error_log(
sprintf(
/* translators: 1: action ID 2: exception message. */
__( 'Action Scheduler was unable to delete action %1$d. Reason: %2$s', 'woocommerce' ),
$id,
$e->getMessage()
)
);
}
}
}
/**
* Implements the logic behind running an action. ActionScheduler_Abstract_ListTable validates the request and their
* parameters are valid.
*
* @param int $action_id
*/
protected function row_action_cancel( $action_id ) {
$this->process_row_action( $action_id, 'cancel' );
}
/**
* Implements the logic behind running an action. ActionScheduler_Abstract_ListTable validates the request and their
* parameters are valid.
*
* @param int $action_id
*/
protected function row_action_run( $action_id ) {
$this->process_row_action( $action_id, 'run' );
}
/**
* Force the data store schema updates.
*/
protected function recreate_tables() {
if ( is_a( $this->store, 'ActionScheduler_HybridStore' ) ) {
$store = $this->store;
} else {
$store = new ActionScheduler_HybridStore();
}
add_action( 'action_scheduler/created_table', array( $store, 'set_autoincrement' ), 10, 2 );
$store_schema = new ActionScheduler_StoreSchema();
$logger_schema = new ActionScheduler_LoggerSchema();
$store_schema->register_tables( true );
$logger_schema->register_tables( true );
remove_action( 'action_scheduler/created_table', array( $store, 'set_autoincrement' ), 10 );
}
/**
* Implements the logic behind processing an action once an action link is clicked on the list table.
*
* @param int $action_id
* @param string $row_action_type The type of action to perform on the action.
*/
protected function process_row_action( $action_id, $row_action_type ) {
try {
switch ( $row_action_type ) {
case 'run' :
$this->runner->process_action( $action_id, 'Admin List Table' );
break;
case 'cancel' :
$this->store->cancel_action( $action_id );
break;
}
$success = 1;
$error_message = '';
} catch ( Exception $e ) {
$success = 0;
$error_message = $e->getMessage();
}
set_transient( 'action_scheduler_admin_notice', compact( 'action_id', 'success', 'error_message', 'row_action_type' ), 30 );
}
/**
* {@inheritDoc}
*/
public function prepare_items() {
$this->prepare_column_headers();
$per_page = $this->get_items_per_page( $this->get_per_page_option_name(), $this->items_per_page );
$query = array(
'per_page' => $per_page,
'offset' => $this->get_items_offset(),
'status' => $this->get_request_status(),
'orderby' => $this->get_request_orderby(),
'order' => $this->get_request_order(),
'search' => $this->get_request_search_query(),
);
/**
* Change query arguments to query for past-due actions.
* Past-due actions have the 'pending' status and are in the past.
* This is needed because registering 'past-due' as a status is overkill.
*/
if ( 'past-due' === $this->get_request_status() ) {
$query['status'] = ActionScheduler_Store::STATUS_PENDING;
$query['date'] = as_get_datetime_object();
}
$this->items = array();
$total_items = $this->store->query_actions( $query, 'count' );
$status_labels = $this->store->get_status_labels();
foreach ( $this->store->query_actions( $query ) as $action_id ) {
try {
$action = $this->store->fetch_action( $action_id );
} catch ( Exception $e ) {
continue;
}
if ( is_a( $action, 'ActionScheduler_NullAction' ) ) {
continue;
}
$this->items[ $action_id ] = array(
'ID' => $action_id,
'hook' => $action->get_hook(),
'status_name' => $this->store->get_status( $action_id ),
'status' => $status_labels[ $this->store->get_status( $action_id ) ],
'args' => $action->get_args(),
'group' => $action->get_group(),
'log_entries' => $this->logger->get_logs( $action_id ),
'claim_id' => $this->store->get_claim_id( $action_id ),
'recurrence' => $this->get_recurrence( $action ),
'schedule' => $action->get_schedule(),
);
}
$this->set_pagination_args( array(
'total_items' => $total_items,
'per_page' => $per_page,
'total_pages' => ceil( $total_items / $per_page ),
) );
}
/**
* Prints the available statuses so the user can click to filter.
*/
protected function display_filter_by_status() {
$this->status_counts = $this->store->action_counts() + $this->store->extra_action_counts();
parent::display_filter_by_status();
}
/**
* Get the text to display in the search box on the list table.
*/
protected function get_search_box_button_text() {
return __( 'Search hook, args and claim ID', 'woocommerce' );
}
/**
* {@inheritDoc}
*/
protected function get_per_page_option_name() {
return str_replace( '-', '_', $this->screen->id ) . '_per_page';
}
}

View File

@@ -0,0 +1,68 @@
<?php
// phpcs:ignoreFile
/**
* Class ActionScheduler_LogEntry
*/
class ActionScheduler_LogEntry {
/**
* @var int $action_id
*/
protected $action_id = '';
/**
* @var string $message
*/
protected $message = '';
/**
* @var Datetime $date
*/
protected $date;
/**
* Constructor
*
* @param mixed $action_id Action ID
* @param string $message Message
* @param Datetime $date Datetime object with the time when this log entry was created. If this parameter is
* not provided a new Datetime object (with current time) will be created.
*/
public function __construct( $action_id, $message, $date = null ) {
/*
* ActionScheduler_wpCommentLogger::get_entry() previously passed a 3rd param of $comment->comment_type
* to ActionScheduler_LogEntry::__construct(), goodness knows why, and the Follow-up Emails plugin
* hard-codes loading its own version of ActionScheduler_wpCommentLogger with that out-dated method,
* goodness knows why, so we need to guard against that here instead of using a DateTime type declaration
* for the constructor's 3rd param of $date and causing a fatal error with older versions of FUE.
*/
if ( null !== $date && ! is_a( $date, 'DateTime' ) ) {
_doing_it_wrong( __METHOD__, 'The third parameter must be a valid DateTime instance, or null.', '2.0.0' );
$date = null;
}
$this->action_id = $action_id;
$this->message = $message;
$this->date = $date ? $date : new Datetime;
}
/**
* Returns the date when this log entry was created
*
* @return Datetime
*/
public function get_date() {
return $this->date;
}
public function get_action_id() {
return $this->action_id;
}
public function get_message() {
return $this->message;
}
}

View File

@@ -0,0 +1,12 @@
<?php
// phpcs:ignoreFile
/**
* Class ActionScheduler_NullLogEntry
*/
class ActionScheduler_NullLogEntry extends ActionScheduler_LogEntry {
public function __construct( $action_id = '', $message = '' ) {
// nothing to see here
}
}

View File

@@ -0,0 +1,136 @@
<?php
// phpcs:ignoreFile
/**
* Provide a way to set simple transient locks to block behaviour
* for up-to a given duration.
*
* Class ActionScheduler_OptionLock
* @since 3.0.0
*/
class ActionScheduler_OptionLock extends ActionScheduler_Lock {
/**
* Set a lock using options for a given amount of time (60 seconds by default).
*
* Using an autoloaded option avoids running database queries or other resource intensive tasks
* on frequently triggered hooks, like 'init' or 'shutdown'.
*
* For example, ActionScheduler_QueueRunner->maybe_dispatch_async_request() uses a lock to avoid
* calling ActionScheduler_QueueRunner->has_maximum_concurrent_batches() every time the 'shutdown',
* hook is triggered, because that method calls ActionScheduler_QueueRunner->store->get_claim_count()
* to find the current number of claims in the database.
*
* @param string $lock_type A string to identify different lock types.
* @bool True if lock value has changed, false if not or if set failed.
*/
public function set( $lock_type ) {
global $wpdb;
$lock_key = $this->get_key( $lock_type );
$existing_lock_value = $this->get_existing_lock( $lock_type );
$new_lock_value = $this->new_lock_value( $lock_type );
// The lock may not exist yet, or may have been deleted.
if ( empty( $existing_lock_value ) ) {
return (bool) $wpdb->insert(
$wpdb->options,
array(
'option_name' => $lock_key,
'option_value' => $new_lock_value,
'autoload' => 'no',
)
);
}
if ( $this->get_expiration_from( $existing_lock_value ) >= time() ) {
return false;
}
// Otherwise, try to obtain the lock.
return (bool) $wpdb->update(
$wpdb->options,
array( 'option_value' => $new_lock_value ),
array(
'option_name' => $lock_key,
'option_value' => $existing_lock_value,
)
);
}
/**
* If a lock is set, return the timestamp it was set to expiry.
*
* @param string $lock_type A string to identify different lock types.
* @return bool|int False if no lock is set, otherwise the timestamp for when the lock is set to expire.
*/
public function get_expiration( $lock_type ) {
return $this->get_expiration_from( $this->get_existing_lock( $lock_type ) );
}
/**
* Given the lock string, derives the lock expiration timestamp (or false if it cannot be determined).
*
* @param string $lock_value String containing a timestamp, or pipe-separated combination of unique value and timestamp.
*
* @return false|int
*/
private function get_expiration_from( $lock_value ) {
$lock_string = explode( '|', $lock_value );
// Old style lock?
if ( count( $lock_string ) === 1 && is_numeric( $lock_string[0] ) ) {
return (int) $lock_string[0];
}
// New style lock?
if ( count( $lock_string ) === 2 && is_numeric( $lock_string[1] ) ) {
return (int) $lock_string[1];
}
return false;
}
/**
* Get the key to use for storing the lock in the transient
*
* @param string $lock_type A string to identify different lock types.
* @return string
*/
protected function get_key( $lock_type ) {
return sprintf( 'action_scheduler_lock_%s', $lock_type );
}
/**
* Supplies the existing lock value, or an empty string if not set.
*
* @param string $lock_type A string to identify different lock types.
*
* @return string
*/
private function get_existing_lock( $lock_type ) {
global $wpdb;
// Now grab the existing lock value, if there is one.
return (string) $wpdb->get_var(
$wpdb->prepare(
"SELECT option_value FROM $wpdb->options WHERE option_name = %s",
$this->get_key( $lock_type )
)
);
}
/**
* Supplies a lock value consisting of a unique value and the current timestamp, which are separated by a pipe
* character.
*
* Example: (string) "649de012e6b262.09774912|1688068114"
*
* @param string $lock_type A string to identify different lock types.
*
* @return string
*/
private function new_lock_value( $lock_type ) {
return uniqid( '', true ) . '|' . ( time() + $this->get_duration( $lock_type ) );
}
}

View File

@@ -0,0 +1,234 @@
<?php
// phpcs:ignoreFile
/**
* Class ActionScheduler_QueueCleaner
*/
class ActionScheduler_QueueCleaner {
/** @var int */
protected $batch_size;
/** @var ActionScheduler_Store */
private $store = null;
/**
* 31 days in seconds.
*
* @var int
*/
private $month_in_seconds = 2678400;
/**
* @var string[] Default list of statuses purged by the cleaner process.
*/
private $default_statuses_to_purge = [
ActionScheduler_Store::STATUS_COMPLETE,
ActionScheduler_Store::STATUS_CANCELED,
];
/**
* ActionScheduler_QueueCleaner constructor.
*
* @param ActionScheduler_Store $store The store instance.
* @param int $batch_size The batch size.
*/
public function __construct( ActionScheduler_Store $store = null, $batch_size = 20 ) {
$this->store = $store ? $store : ActionScheduler_Store::instance();
$this->batch_size = $batch_size;
}
/**
* Default queue cleaner process used by queue runner.
*
* @return array
*/
public function delete_old_actions() {
/**
* Filter the minimum scheduled date age for action deletion.
*
* @param int $retention_period Minimum scheduled age in seconds of the actions to be deleted.
*/
$lifespan = apply_filters( 'action_scheduler_retention_period', $this->month_in_seconds );
try {
$cutoff = as_get_datetime_object( $lifespan . ' seconds ago' );
} catch ( Exception $e ) {
_doing_it_wrong(
__METHOD__,
sprintf(
/* Translators: %s is the exception message. */
esc_html__( 'It was not possible to determine a valid cut-off time: %s.', 'woocommerce' ),
esc_html( $e->getMessage() )
),
'3.5.5'
);
return array();
}
/**
* Filter the statuses when cleaning the queue.
*
* @param string[] $default_statuses_to_purge Action statuses to clean.
*/
$statuses_to_purge = (array) apply_filters( 'action_scheduler_default_cleaner_statuses', $this->default_statuses_to_purge );
return $this->clean_actions( $statuses_to_purge, $cutoff, $this->get_batch_size() );
}
/**
* Delete selected actions limited by status and date.
*
* @param string[] $statuses_to_purge List of action statuses to purge. Defaults to canceled, complete.
* @param DateTime $cutoff_date Date limit for selecting actions. Defaults to 31 days ago.
* @param int|null $batch_size Maximum number of actions per status to delete. Defaults to 20.
* @param string $context Calling process context. Defaults to `old`.
* @return array Actions deleted.
*/
public function clean_actions( array $statuses_to_purge, DateTime $cutoff_date, $batch_size = null, $context = 'old' ) {
$batch_size = $batch_size !== null ? $batch_size : $this->batch_size;
$cutoff = $cutoff_date !== null ? $cutoff_date : as_get_datetime_object( $this->month_in_seconds . ' seconds ago' );
$lifespan = time() - $cutoff->getTimestamp();
if ( empty( $statuses_to_purge ) ) {
$statuses_to_purge = $this->default_statuses_to_purge;
}
$deleted_actions = [];
foreach ( $statuses_to_purge as $status ) {
$actions_to_delete = $this->store->query_actions( array(
'status' => $status,
'modified' => $cutoff,
'modified_compare' => '<=',
'per_page' => $batch_size,
'orderby' => 'none',
) );
$deleted_actions = array_merge( $deleted_actions, $this->delete_actions( $actions_to_delete, $lifespan, $context ) );
}
return $deleted_actions;
}
/**
* @param int[] $actions_to_delete List of action IDs to delete.
* @param int $lifespan Minimum scheduled age in seconds of the actions being deleted.
* @param string $context Context of the delete request.
* @return array Deleted action IDs.
*/
private function delete_actions( array $actions_to_delete, $lifespan = null, $context = 'old' ) {
$deleted_actions = [];
if ( $lifespan === null ) {
$lifespan = $this->month_in_seconds;
}
foreach ( $actions_to_delete as $action_id ) {
try {
$this->store->delete_action( $action_id );
$deleted_actions[] = $action_id;
} catch ( Exception $e ) {
/**
* Notify 3rd party code of exceptions when deleting a completed action older than the retention period
*
* This hook provides a way for 3rd party code to log or otherwise handle exceptions relating to their
* actions.
*
* @param int $action_id The scheduled actions ID in the data store
* @param Exception $e The exception thrown when attempting to delete the action from the data store
* @param int $lifespan The retention period, in seconds, for old actions
* @param int $count_of_actions_to_delete The number of old actions being deleted in this batch
* @since 2.0.0
*
*/
do_action( "action_scheduler_failed_{$context}_action_deletion", $action_id, $e, $lifespan, count( $actions_to_delete ) );
}
}
return $deleted_actions;
}
/**
* Unclaim pending actions that have not been run within a given time limit.
*
* When called by ActionScheduler_Abstract_QueueRunner::run_cleanup(), the time limit passed
* as a parameter is 10x the time limit used for queue processing.
*
* @param int $time_limit The number of seconds to allow a queue to run before unclaiming its pending actions. Default 300 (5 minutes).
*/
public function reset_timeouts( $time_limit = 300 ) {
$timeout = apply_filters( 'action_scheduler_timeout_period', $time_limit );
if ( $timeout < 0 ) {
return;
}
$cutoff = as_get_datetime_object($timeout.' seconds ago');
$actions_to_reset = $this->store->query_actions( array(
'status' => ActionScheduler_Store::STATUS_PENDING,
'modified' => $cutoff,
'modified_compare' => '<=',
'claimed' => true,
'per_page' => $this->get_batch_size(),
'orderby' => 'none',
) );
foreach ( $actions_to_reset as $action_id ) {
$this->store->unclaim_action( $action_id );
do_action( 'action_scheduler_reset_action', $action_id );
}
}
/**
* Mark actions that have been running for more than a given time limit as failed, based on
* the assumption some uncatachable and unloggable fatal error occurred during processing.
*
* When called by ActionScheduler_Abstract_QueueRunner::run_cleanup(), the time limit passed
* as a parameter is 10x the time limit used for queue processing.
*
* @param int $time_limit The number of seconds to allow an action to run before it is considered to have failed. Default 300 (5 minutes).
*/
public function mark_failures( $time_limit = 300 ) {
$timeout = apply_filters( 'action_scheduler_failure_period', $time_limit );
if ( $timeout < 0 ) {
return;
}
$cutoff = as_get_datetime_object($timeout.' seconds ago');
$actions_to_reset = $this->store->query_actions( array(
'status' => ActionScheduler_Store::STATUS_RUNNING,
'modified' => $cutoff,
'modified_compare' => '<=',
'per_page' => $this->get_batch_size(),
'orderby' => 'none',
) );
foreach ( $actions_to_reset as $action_id ) {
$this->store->mark_failure( $action_id );
do_action( 'action_scheduler_failed_action', $action_id, $timeout );
}
}
/**
* Do all of the cleaning actions.
*
* @param int $time_limit The number of seconds to use as the timeout and failure period. Default 300 (5 minutes).
* @author Jeremy Pry
*/
public function clean( $time_limit = 300 ) {
$this->delete_old_actions();
$this->reset_timeouts( $time_limit );
$this->mark_failures( $time_limit );
}
/**
* Get the batch size for cleaning the queue.
*
* @author Jeremy Pry
* @return int
*/
protected function get_batch_size() {
/**
* Filter the batch size when cleaning the queue.
*
* @param int $batch_size The number of actions to clean in one batch.
*/
return absint( apply_filters( 'action_scheduler_cleanup_batch_size', $this->batch_size ) );
}
}

View File

@@ -0,0 +1,230 @@
<?php
// phpcs:ignoreFile
/**
* Class ActionScheduler_QueueRunner
*/
class ActionScheduler_QueueRunner extends ActionScheduler_Abstract_QueueRunner {
const WP_CRON_HOOK = 'action_scheduler_run_queue';
const WP_CRON_SCHEDULE = 'every_minute';
/** @var ActionScheduler_AsyncRequest_QueueRunner */
protected $async_request;
/** @var ActionScheduler_QueueRunner */
private static $runner = null;
/** @var int */
private $processed_actions_count = 0;
/**
* @return ActionScheduler_QueueRunner
* @codeCoverageIgnore
*/
public static function instance() {
if ( empty(self::$runner) ) {
$class = apply_filters('action_scheduler_queue_runner_class', 'ActionScheduler_QueueRunner');
self::$runner = new $class();
}
return self::$runner;
}
/**
* ActionScheduler_QueueRunner constructor.
*
* @param ActionScheduler_Store $store
* @param ActionScheduler_FatalErrorMonitor $monitor
* @param ActionScheduler_QueueCleaner $cleaner
*/
public function __construct( ActionScheduler_Store $store = null, ActionScheduler_FatalErrorMonitor $monitor = null, ActionScheduler_QueueCleaner $cleaner = null, ActionScheduler_AsyncRequest_QueueRunner $async_request = null ) {
parent::__construct( $store, $monitor, $cleaner );
if ( is_null( $async_request ) ) {
$async_request = new ActionScheduler_AsyncRequest_QueueRunner( $this->store );
}
$this->async_request = $async_request;
}
/**
* @codeCoverageIgnore
*/
public function init() {
add_filter( 'cron_schedules', array( self::instance(), 'add_wp_cron_schedule' ) );
// Check for and remove any WP Cron hook scheduled by Action Scheduler < 3.0.0, which didn't include the $context param
$next_timestamp = wp_next_scheduled( self::WP_CRON_HOOK );
if ( $next_timestamp ) {
wp_unschedule_event( $next_timestamp, self::WP_CRON_HOOK );
}
$cron_context = array( 'WP Cron' );
if ( ! wp_next_scheduled( self::WP_CRON_HOOK, $cron_context ) ) {
$schedule = apply_filters( 'action_scheduler_run_schedule', self::WP_CRON_SCHEDULE );
wp_schedule_event( time(), $schedule, self::WP_CRON_HOOK, $cron_context );
}
add_action( self::WP_CRON_HOOK, array( self::instance(), 'run' ) );
$this->hook_dispatch_async_request();
}
/**
* Hook check for dispatching an async request.
*/
public function hook_dispatch_async_request() {
add_action( 'shutdown', array( $this, 'maybe_dispatch_async_request' ) );
}
/**
* Unhook check for dispatching an async request.
*/
public function unhook_dispatch_async_request() {
remove_action( 'shutdown', array( $this, 'maybe_dispatch_async_request' ) );
}
/**
* Check if we should dispatch an async request to process actions.
*
* This method is attached to 'shutdown', so is called frequently. To avoid slowing down
* the site, it mitigates the work performed in each request by:
* 1. checking if it's in the admin context and then
* 2. haven't run on the 'shutdown' hook within the lock time (60 seconds by default)
* 3. haven't exceeded the number of allowed batches.
*
* The order of these checks is important, because they run from a check on a value:
* 1. in memory - is_admin() maps to $GLOBALS or the WP_ADMIN constant
* 2. in memory - transients use autoloaded options by default
* 3. from a database query - has_maximum_concurrent_batches() run the query
* $this->store->get_claim_count() to find the current number of claims in the DB.
*
* If all of these conditions are met, then we request an async runner check whether it
* should dispatch a request to process pending actions.
*/
public function maybe_dispatch_async_request() {
// Only start an async queue at most once every 60 seconds.
if (
is_admin()
&& ! ActionScheduler::lock()->is_locked( 'async-request-runner' )
&& ActionScheduler::lock()->set( 'async-request-runner' )
) {
$this->async_request->maybe_dispatch();
}
}
/**
* Process actions in the queue. Attached to self::WP_CRON_HOOK i.e. 'action_scheduler_run_queue'
*
* The $context param of this method defaults to 'WP Cron', because prior to Action Scheduler 3.0.0
* that was the only context in which this method was run, and the self::WP_CRON_HOOK hook had no context
* passed along with it. New code calling this method directly, or by triggering the self::WP_CRON_HOOK,
* should set a context as the first parameter. For an example of this, refer to the code seen in
* @see ActionScheduler_AsyncRequest_QueueRunner::handle()
*
* @param string $context Optional identifer for the context in which this action is being processed, e.g. 'WP CLI' or 'WP Cron'
* Generally, this should be capitalised and not localised as it's a proper noun.
* @return int The number of actions processed.
*/
public function run( $context = 'WP Cron' ) {
ActionScheduler_Compatibility::raise_memory_limit();
ActionScheduler_Compatibility::raise_time_limit( $this->get_time_limit() );
do_action( 'action_scheduler_before_process_queue' );
$this->run_cleanup();
$this->processed_actions_count = 0;
if ( false === $this->has_maximum_concurrent_batches() ) {
$batch_size = apply_filters( 'action_scheduler_queue_runner_batch_size', 25 );
do {
$processed_actions_in_batch = $this->do_batch( $batch_size, $context );
$this->processed_actions_count += $processed_actions_in_batch;
} while ( $processed_actions_in_batch > 0 && ! $this->batch_limits_exceeded( $this->processed_actions_count ) ); // keep going until we run out of actions, time, or memory
}
do_action( 'action_scheduler_after_process_queue' );
return $this->processed_actions_count;
}
/**
* Process a batch of actions pending in the queue.
*
* Actions are processed by claiming a set of pending actions then processing each one until either the batch
* size is completed, or memory or time limits are reached, defined by @see $this->batch_limits_exceeded().
*
* @param int $size The maximum number of actions to process in the batch.
* @param string $context Optional identifer for the context in which this action is being processed, e.g. 'WP CLI' or 'WP Cron'
* Generally, this should be capitalised and not localised as it's a proper noun.
* @return int The number of actions processed.
*/
protected function do_batch( $size = 100, $context = '' ) {
$claim = $this->store->stake_claim($size);
$this->monitor->attach($claim);
$processed_actions = 0;
foreach ( $claim->get_actions() as $action_id ) {
// bail if we lost the claim
if ( ! in_array( $action_id, $this->store->find_actions_by_claim_id( $claim->get_id() ) ) ) {
break;
}
$this->process_action( $action_id, $context );
$processed_actions++;
if ( $this->batch_limits_exceeded( $processed_actions + $this->processed_actions_count ) ) {
break;
}
}
$this->store->release_claim($claim);
$this->monitor->detach();
$this->clear_caches();
return $processed_actions;
}
/**
* Flush the cache if possible (intended for use after a batch of actions has been processed).
*
* This is useful because running large batches can eat up memory and because invalid data can accrue in the
* runtime cache, which may lead to unexpected results.
*/
protected function clear_caches() {
/*
* Calling wp_cache_flush_runtime() lets us clear the runtime cache without invalidating the external object
* cache, so we will always prefer this method (as compared to calling wp_cache_flush()) when it is available.
*
* However, this function was only introduced in WordPress 6.0. Additionally, the preferred way of detecting if
* it is supported changed in WordPress 6.1 so we use two different methods to decide if we should utilize it.
*/
$flushing_runtime_cache_explicitly_supported = function_exists( 'wp_cache_supports' ) && wp_cache_supports( 'flush_runtime' );
$flushing_runtime_cache_implicitly_supported = ! function_exists( 'wp_cache_supports' ) && function_exists( 'wp_cache_flush_runtime' );
if ( $flushing_runtime_cache_explicitly_supported || $flushing_runtime_cache_implicitly_supported ) {
wp_cache_flush_runtime();
} elseif (
! wp_using_ext_object_cache()
/**
* When an external object cache is in use, and when wp_cache_flush_runtime() is not available, then
* normally the cache will not be flushed after processing a batch of actions (to avoid a performance
* penalty for other processes).
*
* This filter makes it possible to override this behavior and always flush the cache, even if an external
* object cache is in use.
*
* @since 1.0
*
* @param bool $flush_cache If the cache should be flushed.
*/
|| apply_filters( 'action_scheduler_queue_runner_flush_cache', false )
) {
wp_cache_flush();
}
}
public function add_wp_cron_schedule( $schedules ) {
$schedules['every_minute'] = array(
'interval' => 60, // in seconds
'display' => __( 'Every minute', 'woocommerce' ),
);
return $schedules;
}
}

View File

@@ -0,0 +1,63 @@
<?php
// phpcs:ignoreFile
/**
* Class ActionScheduler_Versions
*/
class ActionScheduler_Versions {
/**
* @var ActionScheduler_Versions
*/
private static $instance = NULL;
private $versions = array();
public function register( $version_string, $initialization_callback ) {
if ( isset($this->versions[$version_string]) ) {
return FALSE;
}
$this->versions[$version_string] = $initialization_callback;
return TRUE;
}
public function get_versions() {
return $this->versions;
}
public function latest_version() {
$keys = array_keys($this->versions);
if ( empty($keys) ) {
return false;
}
uasort( $keys, 'version_compare' );
return end($keys);
}
public function latest_version_callback() {
$latest = $this->latest_version();
if ( empty($latest) || !isset($this->versions[$latest]) ) {
return '__return_null';
}
return $this->versions[$latest];
}
/**
* @return ActionScheduler_Versions
* @codeCoverageIgnore
*/
public static function instance() {
if ( empty(self::$instance) ) {
self::$instance = new self();
}
return self::$instance;
}
/**
* @codeCoverageIgnore
*/
public static function initialize_latest_version() {
$self = self::instance();
call_user_func($self->latest_version_callback());
}
}

View File

@@ -0,0 +1,116 @@
<?php
// phpcs:ignoreFile
/**
* Class ActionScheduler_WPCommentCleaner
*
* @since 3.0.0
*/
class ActionScheduler_WPCommentCleaner {
/**
* Post migration hook used to cleanup the WP comment table.
*
* @var string
*/
protected static $cleanup_hook = 'action_scheduler/cleanup_wp_comment_logs';
/**
* An instance of the ActionScheduler_wpCommentLogger class to interact with the comments table.
*
* This instance should only be used as an interface. It should not be initialized.
*
* @var ActionScheduler_wpCommentLogger
*/
protected static $wp_comment_logger = null;
/**
* The key used to store the cached value of whether there are logs in the WP comment table.
*
* @var string
*/
protected static $has_logs_option_key = 'as_has_wp_comment_logs';
/**
* Initialize the class and attach callbacks.
*/
public static function init() {
if ( empty( self::$wp_comment_logger ) ) {
self::$wp_comment_logger = new ActionScheduler_wpCommentLogger();
}
add_action( self::$cleanup_hook, array( __CLASS__, 'delete_all_action_comments' ) );
// While there are orphaned logs left in the comments table, we need to attach the callbacks which filter comment counts.
add_action( 'pre_get_comments', array( self::$wp_comment_logger, 'filter_comment_queries' ), 10, 1 );
add_action( 'wp_count_comments', array( self::$wp_comment_logger, 'filter_comment_count' ), 20, 2 ); // run after WC_Comments::wp_count_comments() to make sure we exclude order notes and action logs
add_action( 'comment_feed_where', array( self::$wp_comment_logger, 'filter_comment_feed' ), 10, 2 );
// Action Scheduler may be displayed as a Tools screen or WooCommerce > Status administration screen
add_action( 'load-tools_page_action-scheduler', array( __CLASS__, 'register_admin_notice' ) );
add_action( 'load-woocommerce_page_wc-status', array( __CLASS__, 'register_admin_notice' ) );
}
/**
* Determines if there are log entries in the wp comments table.
*
* Uses the flag set on migration completion set by @see self::maybe_schedule_cleanup().
*
* @return boolean Whether there are scheduled action comments in the comments table.
*/
public static function has_logs() {
return 'yes' === get_option( self::$has_logs_option_key );
}
/**
* Schedules the WP Post comment table cleanup to run in 6 months if it's not already scheduled.
* Attached to the migration complete hook 'action_scheduler/migration_complete'.
*/
public static function maybe_schedule_cleanup() {
if ( (bool) get_comments( array( 'type' => ActionScheduler_wpCommentLogger::TYPE, 'number' => 1, 'fields' => 'ids' ) ) ) {
update_option( self::$has_logs_option_key, 'yes' );
if ( ! as_next_scheduled_action( self::$cleanup_hook ) ) {
as_schedule_single_action( gmdate( 'U' ) + ( 6 * MONTH_IN_SECONDS ), self::$cleanup_hook );
}
}
}
/**
* Delete all action comments from the WP Comments table.
*/
public static function delete_all_action_comments() {
global $wpdb;
$wpdb->delete( $wpdb->comments, array( 'comment_type' => ActionScheduler_wpCommentLogger::TYPE, 'comment_agent' => ActionScheduler_wpCommentLogger::AGENT ) );
delete_option( self::$has_logs_option_key );
}
/**
* Registers admin notices about the orphaned action logs.
*/
public static function register_admin_notice() {
add_action( 'admin_notices', array( __CLASS__, 'print_admin_notice' ) );
}
/**
* Prints details about the orphaned action logs and includes information on where to learn more.
*/
public static function print_admin_notice() {
$next_cleanup_message = '';
$next_scheduled_cleanup_hook = as_next_scheduled_action( self::$cleanup_hook );
if ( $next_scheduled_cleanup_hook ) {
/* translators: %s: date interval */
$next_cleanup_message = sprintf( __( 'This data will be deleted in %s.', 'woocommerce' ), human_time_diff( gmdate( 'U' ), $next_scheduled_cleanup_hook ) );
}
$notice = sprintf(
/* translators: 1: next cleanup message 2: github issue URL */
__( 'Action Scheduler has migrated data to custom tables; however, orphaned log entries exist in the WordPress Comments table. %1$s <a href="%2$s">Learn more &raquo;</a>', 'woocommerce' ),
$next_cleanup_message,
'https://github.com/woocommerce/action-scheduler/issues/368'
);
echo '<div class="notice notice-warning"><p>' . wp_kses_post( $notice ) . '</p></div>';
}
}

View File

@@ -0,0 +1,167 @@
<?php
// phpcs:ignoreFile
/**
* Class ActionScheduler_wcSystemStatus
*/
class ActionScheduler_wcSystemStatus {
/**
* The active data stores
*
* @var ActionScheduler_Store
*/
protected $store;
/**
* Constructor method for ActionScheduler_wcSystemStatus.
*
* @param ActionScheduler_Store $store Active store object.
*
* @return void
*/
public function __construct( $store ) {
$this->store = $store;
}
/**
* Display action data, including number of actions grouped by status and the oldest & newest action in each status.
*
* Helpful to identify issues, like a clogged queue.
*/
public function render() {
$action_counts = $this->store->action_counts();
$status_labels = $this->store->get_status_labels();
$oldest_and_newest = $this->get_oldest_and_newest( array_keys( $status_labels ) );
$this->get_template( $status_labels, $action_counts, $oldest_and_newest );
}
/**
* Get oldest and newest scheduled dates for a given set of statuses.
*
* @param array $status_keys Set of statuses to find oldest & newest action for.
* @return array
*/
protected function get_oldest_and_newest( $status_keys ) {
$oldest_and_newest = array();
foreach ( $status_keys as $status ) {
$oldest_and_newest[ $status ] = array(
'oldest' => '&ndash;',
'newest' => '&ndash;',
);
if ( 'in-progress' === $status ) {
continue;
}
$oldest_and_newest[ $status ]['oldest'] = $this->get_action_status_date( $status, 'oldest' );
$oldest_and_newest[ $status ]['newest'] = $this->get_action_status_date( $status, 'newest' );
}
return $oldest_and_newest;
}
/**
* Get oldest or newest scheduled date for a given status.
*
* @param string $status Action status label/name string.
* @param string $date_type Oldest or Newest.
* @return DateTime
*/
protected function get_action_status_date( $status, $date_type = 'oldest' ) {
$order = 'oldest' === $date_type ? 'ASC' : 'DESC';
$action = $this->store->query_actions(
array(
'claimed' => false,
'status' => $status,
'per_page' => 1,
'order' => $order,
)
);
if ( ! empty( $action ) ) {
$date_object = $this->store->get_date( $action[0] );
$action_date = $date_object->format( 'Y-m-d H:i:s O' );
} else {
$action_date = '&ndash;';
}
return $action_date;
}
/**
* Get oldest or newest scheduled date for a given status.
*
* @param array $status_labels Set of statuses to find oldest & newest action for.
* @param array $action_counts Number of actions grouped by status.
* @param array $oldest_and_newest Date of the oldest and newest action with each status.
*/
protected function get_template( $status_labels, $action_counts, $oldest_and_newest ) {
$as_version = ActionScheduler_Versions::instance()->latest_version();
$as_datastore = get_class( ActionScheduler_Store::instance() );
?>
<table class="wc_status_table widefat" cellspacing="0">
<thead>
<tr>
<th colspan="5" data-export-label="Action Scheduler"><h2><?php esc_html_e( 'Action Scheduler', 'woocommerce' ); ?><?php echo wc_help_tip( esc_html__( 'This section shows details of Action Scheduler.', 'woocommerce' ) ); ?></h2></th>
</tr>
<tr>
<td colspan="2" data-export-label="Version"><?php esc_html_e( 'Version:', 'woocommerce' ); ?></td>
<td colspan="3"><?php echo esc_html( $as_version ); ?></td>
</tr>
<tr>
<td colspan="2" data-export-label="Data store"><?php esc_html_e( 'Data store:', 'woocommerce' ); ?></td>
<td colspan="3"><?php echo esc_html( $as_datastore ); ?></td>
</tr>
<tr>
<td><strong><?php esc_html_e( 'Action Status', 'woocommerce' ); ?></strong></td>
<td class="help">&nbsp;</td>
<td><strong><?php esc_html_e( 'Count', 'woocommerce' ); ?></strong></td>
<td><strong><?php esc_html_e( 'Oldest Scheduled Date', 'woocommerce' ); ?></strong></td>
<td><strong><?php esc_html_e( 'Newest Scheduled Date', 'woocommerce' ); ?></strong></td>
</tr>
</thead>
<tbody>
<?php
foreach ( $action_counts as $status => $count ) {
// WC uses the 3rd column for export, so we need to display more data in that (hidden when viewed as part of the table) and add an empty 2nd column.
printf(
'<tr><td>%1$s</td><td>&nbsp;</td><td>%2$s<span style="display: none;">, Oldest: %3$s, Newest: %4$s</span></td><td>%3$s</td><td>%4$s</td></tr>',
esc_html( $status_labels[ $status ] ),
esc_html( number_format_i18n( $count ) ),
esc_html( $oldest_and_newest[ $status ]['oldest'] ),
esc_html( $oldest_and_newest[ $status ]['newest'] )
);
}
?>
</tbody>
</table>
<?php
}
/**
* Is triggered when invoking inaccessible methods in an object context.
*
* @param string $name Name of method called.
* @param array $arguments Parameters to invoke the method with.
*
* @return mixed
* @link https://php.net/manual/en/language.oop5.overloading.php#language.oop5.overloading.methods
*/
public function __call( $name, $arguments ) {
switch ( $name ) {
case 'print':
_deprecated_function( __CLASS__ . '::print()', '2.2.4', __CLASS__ . '::render()' );
return call_user_func_array( array( $this, 'render' ), $arguments );
}
return null;
}
}

View File

@@ -0,0 +1,126 @@
<?php
// phpcs:ignoreFile
/**
* Commands for Action Scheduler.
*/
class ActionScheduler_WPCLI_Clean_Command extends WP_CLI_Command {
/**
* Run the Action Scheduler Queue Cleaner
*
* ## OPTIONS
*
* [--batch-size=<size>]
* : The maximum number of actions to delete per batch. Defaults to 20.
*
* [--batches=<size>]
* : Limit execution to a number of batches. Defaults to 0, meaning batches will continue all eligible actions are deleted.
*
* [--status=<status>]
* : Only clean actions with the specified status. Defaults to Canceled, Completed. Define multiple statuses as a comma separated string (without spaces), e.g. `--status=complete,failed,canceled`
*
* [--before=<datestring>]
* : Only delete actions with scheduled date older than this. Defaults to 31 days. e.g `--before='7 days ago'`, `--before='02-Feb-2020 20:20:20'`
*
* [--pause=<seconds>]
* : The number of seconds to pause between batches. Default no pause.
*
* @param array $args Positional arguments.
* @param array $assoc_args Keyed arguments.
* @throws \WP_CLI\ExitException When an error occurs.
*
* @subcommand clean
*/
public function clean( $args, $assoc_args ) {
// Handle passed arguments.
$batch = absint( \WP_CLI\Utils\get_flag_value( $assoc_args, 'batch-size', 20 ) );
$batches = absint( \WP_CLI\Utils\get_flag_value( $assoc_args, 'batches', 0 ) );
$status = explode( ',', WP_CLI\Utils\get_flag_value( $assoc_args, 'status', '' ) );
$status = array_filter( array_map( 'trim', $status ) );
$before = \WP_CLI\Utils\get_flag_value( $assoc_args, 'before', '' );
$sleep = \WP_CLI\Utils\get_flag_value( $assoc_args, 'pause', 0 );
$batches_completed = 0;
$actions_deleted = 0;
$unlimited = $batches === 0;
try {
$lifespan = as_get_datetime_object( $before );
} catch ( Exception $e ) {
$lifespan = null;
}
try {
// Custom queue cleaner instance.
$cleaner = new ActionScheduler_QueueCleaner( null, $batch );
// Clean actions for as long as possible.
while ( $unlimited || $batches_completed < $batches ) {
if ( $sleep && $batches_completed > 0 ) {
sleep( $sleep );
}
$deleted = count( $cleaner->clean_actions( $status, $lifespan, null,'CLI' ) );
if ( $deleted <= 0 ) {
break;
}
$actions_deleted += $deleted;
$batches_completed++;
$this->print_success( $deleted );
}
} catch ( Exception $e ) {
$this->print_error( $e );
}
$this->print_total_batches( $batches_completed );
if ( $batches_completed > 1 ) {
$this->print_success( $actions_deleted );
}
}
/**
* Print WP CLI message about how many batches of actions were processed.
*
* @param int $batches_processed
*/
protected function print_total_batches( int $batches_processed ) {
WP_CLI::log(
sprintf(
/* translators: %d refers to the total number of batches processed */
_n( '%d batch processed.', '%d batches processed.', $batches_processed, 'woocommerce' ),
$batches_processed
)
);
}
/**
* Convert an exception into a WP CLI error.
*
* @param Exception $e The error object.
*
* @throws \WP_CLI\ExitException
*/
protected function print_error( Exception $e ) {
WP_CLI::error(
sprintf(
/* translators: %s refers to the exception error message */
__( 'There was an error deleting an action: %s', 'woocommerce' ),
$e->getMessage()
)
);
}
/**
* Print a success message with the number of completed actions.
*
* @param int $actions_deleted
*/
protected function print_success( int $actions_deleted ) {
WP_CLI::success(
sprintf(
/* translators: %d refers to the total number of actions deleted */
_n( '%d action deleted.', '%d actions deleted.', $actions_deleted, 'woocommerce' ),
$actions_deleted
)
);
}
}

View File

@@ -0,0 +1,198 @@
<?php
// phpcs:ignoreFile
use Action_Scheduler\WP_CLI\ProgressBar;
/**
* WP CLI Queue runner.
*
* This class can only be called from within a WP CLI instance.
*/
class ActionScheduler_WPCLI_QueueRunner extends ActionScheduler_Abstract_QueueRunner {
/** @var array */
protected $actions;
/** @var ActionScheduler_ActionClaim */
protected $claim;
/** @var \cli\progress\Bar */
protected $progress_bar;
/**
* ActionScheduler_WPCLI_QueueRunner constructor.
*
* @param ActionScheduler_Store $store
* @param ActionScheduler_FatalErrorMonitor $monitor
* @param ActionScheduler_QueueCleaner $cleaner
*
* @throws Exception When this is not run within WP CLI
*/
public function __construct( ActionScheduler_Store $store = null, ActionScheduler_FatalErrorMonitor $monitor = null, ActionScheduler_QueueCleaner $cleaner = null ) {
if ( ! ( defined( 'WP_CLI' ) && WP_CLI ) ) {
/* translators: %s php class name */
throw new Exception( sprintf( __( 'The %s class can only be run within WP CLI.', 'woocommerce' ), __CLASS__ ) );
}
parent::__construct( $store, $monitor, $cleaner );
}
/**
* Set up the Queue before processing.
*
* @author Jeremy Pry
*
* @param int $batch_size The batch size to process.
* @param array $hooks The hooks being used to filter the actions claimed in this batch.
* @param string $group The group of actions to claim with this batch.
* @param bool $force Whether to force running even with too many concurrent processes.
*
* @return int The number of actions that will be run.
* @throws \WP_CLI\ExitException When there are too many concurrent batches.
*/
public function setup( $batch_size, $hooks = array(), $group = '', $force = false ) {
$this->run_cleanup();
$this->add_hooks();
// Check to make sure there aren't too many concurrent processes running.
if ( $this->has_maximum_concurrent_batches() ) {
if ( $force ) {
WP_CLI::warning( __( 'There are too many concurrent batches, but the run is forced to continue.', 'woocommerce' ) );
} else {
WP_CLI::error( __( 'There are too many concurrent batches.', 'woocommerce' ) );
}
}
// Stake a claim and store it.
$this->claim = $this->store->stake_claim( $batch_size, null, $hooks, $group );
$this->monitor->attach( $this->claim );
$this->actions = $this->claim->get_actions();
return count( $this->actions );
}
/**
* Add our hooks to the appropriate actions.
*
* @author Jeremy Pry
*/
protected function add_hooks() {
add_action( 'action_scheduler_before_execute', array( $this, 'before_execute' ) );
add_action( 'action_scheduler_after_execute', array( $this, 'after_execute' ), 10, 2 );
add_action( 'action_scheduler_failed_execution', array( $this, 'action_failed' ), 10, 2 );
}
/**
* Set up the WP CLI progress bar.
*
* @author Jeremy Pry
*/
protected function setup_progress_bar() {
$count = count( $this->actions );
$this->progress_bar = new ProgressBar(
/* translators: %d: amount of actions */
sprintf( _n( 'Running %d action', 'Running %d actions', $count, 'woocommerce' ), $count ),
$count
);
}
/**
* Process actions in the queue.
*
* @author Jeremy Pry
*
* @param string $context Optional runner context. Default 'WP CLI'.
*
* @return int The number of actions processed.
*/
public function run( $context = 'WP CLI' ) {
do_action( 'action_scheduler_before_process_queue' );
$this->setup_progress_bar();
foreach ( $this->actions as $action_id ) {
// Error if we lost the claim.
if ( ! in_array( $action_id, $this->store->find_actions_by_claim_id( $this->claim->get_id() ) ) ) {
WP_CLI::warning( __( 'The claim has been lost. Aborting current batch.', 'woocommerce' ) );
break;
}
$this->process_action( $action_id, $context );
$this->progress_bar->tick();
}
$completed = $this->progress_bar->current();
$this->progress_bar->finish();
$this->store->release_claim( $this->claim );
do_action( 'action_scheduler_after_process_queue' );
return $completed;
}
/**
* Handle WP CLI message when the action is starting.
*
* @author Jeremy Pry
*
* @param $action_id
*/
public function before_execute( $action_id ) {
/* translators: %s refers to the action ID */
WP_CLI::log( sprintf( __( 'Started processing action %s', 'woocommerce' ), $action_id ) );
}
/**
* Handle WP CLI message when the action has completed.
*
* @author Jeremy Pry
*
* @param int $action_id
* @param null|ActionScheduler_Action $action The instance of the action. Default to null for backward compatibility.
*/
public function after_execute( $action_id, $action = null ) {
// backward compatibility
if ( null === $action ) {
$action = $this->store->fetch_action( $action_id );
}
/* translators: 1: action ID 2: hook name */
WP_CLI::log( sprintf( __( 'Completed processing action %1$s with hook: %2$s', 'woocommerce' ), $action_id, $action->get_hook() ) );
}
/**
* Handle WP CLI message when the action has failed.
*
* @author Jeremy Pry
*
* @param int $action_id
* @param Exception $exception
* @throws \WP_CLI\ExitException With failure message.
*/
public function action_failed( $action_id, $exception ) {
WP_CLI::error(
/* translators: 1: action ID 2: exception message */
sprintf( __( 'Error processing action %1$s: %2$s', 'woocommerce' ), $action_id, $exception->getMessage() ),
false
);
}
/**
* Sleep and help avoid hitting memory limit
*
* @param int $sleep_time Amount of seconds to sleep
* @deprecated 3.0.0
*/
protected function stop_the_insanity( $sleep_time = 0 ) {
_deprecated_function( 'ActionScheduler_WPCLI_QueueRunner::stop_the_insanity', '3.0.0', 'ActionScheduler_DataController::free_memory' );
ActionScheduler_DataController::free_memory();
}
/**
* Maybe trigger the stop_the_insanity() method to free up memory.
*/
protected function maybe_stop_the_insanity() {
// The value returned by progress_bar->current() might be padded. Remove padding, and convert to int.
$current_iteration = intval( trim( $this->progress_bar->current() ) );
if ( 0 === $current_iteration % 50 ) {
$this->stop_the_insanity();
}
}
}

View File

@@ -0,0 +1,211 @@
<?php
// phpcs:ignoreFile
/**
* Commands for Action Scheduler.
*/
class ActionScheduler_WPCLI_Scheduler_command extends WP_CLI_Command {
/**
* Force tables schema creation for Action Scheduler
*
* ## OPTIONS
*
* @param array $args Positional arguments.
* @param array $assoc_args Keyed arguments.
*
* @subcommand fix-schema
*/
public function fix_schema( $args, $assoc_args ) {
$schema_classes = array( ActionScheduler_LoggerSchema::class, ActionScheduler_StoreSchema::class );
foreach ( $schema_classes as $classname ) {
if ( is_subclass_of( $classname, ActionScheduler_Abstract_Schema::class ) ) {
$obj = new $classname();
$obj->init();
$obj->register_tables( true );
WP_CLI::success(
sprintf(
/* translators: %s refers to the schema name*/
__( 'Registered schema for %s', 'woocommerce' ),
$classname
)
);
}
}
}
/**
* Run the Action Scheduler
*
* ## OPTIONS
*
* [--batch-size=<size>]
* : The maximum number of actions to run. Defaults to 100.
*
* [--batches=<size>]
* : Limit execution to a number of batches. Defaults to 0, meaning batches will continue being executed until all actions are complete.
*
* [--cleanup-batch-size=<size>]
* : The maximum number of actions to clean up. Defaults to the value of --batch-size.
*
* [--hooks=<hooks>]
* : Only run actions with the specified hook. Omitting this option runs actions with any hook. Define multiple hooks as a comma separated string (without spaces), e.g. `--hooks=hook_one,hook_two,hook_three`
*
* [--group=<group>]
* : Only run actions from the specified group. Omitting this option runs actions from all groups.
*
* [--exclude-groups=<groups>]
* : Run actions from all groups except the specified group(s). Define multiple groups as a comma separated string (without spaces), e.g. '--group_a,group_b'. This option is ignored when `--group` is used.
*
* [--free-memory-on=<count>]
* : The number of actions to process between freeing memory. 0 disables freeing memory. Default 50.
*
* [--pause=<seconds>]
* : The number of seconds to pause when freeing memory. Default no pause.
*
* [--force]
* : Whether to force execution despite the maximum number of concurrent processes being exceeded.
*
* @param array $args Positional arguments.
* @param array $assoc_args Keyed arguments.
* @throws \WP_CLI\ExitException When an error occurs.
*
* @subcommand run
*/
public function run( $args, $assoc_args ) {
// Handle passed arguments.
$batch = absint( \WP_CLI\Utils\get_flag_value( $assoc_args, 'batch-size', 100 ) );
$batches = absint( \WP_CLI\Utils\get_flag_value( $assoc_args, 'batches', 0 ) );
$clean = absint( \WP_CLI\Utils\get_flag_value( $assoc_args, 'cleanup-batch-size', $batch ) );
$hooks = explode( ',', WP_CLI\Utils\get_flag_value( $assoc_args, 'hooks', '' ) );
$hooks = array_filter( array_map( 'trim', $hooks ) );
$group = \WP_CLI\Utils\get_flag_value( $assoc_args, 'group', '' );
$exclude_groups = \WP_CLI\Utils\get_flag_value( $assoc_args, 'exclude-groups', '' );
$free_on = \WP_CLI\Utils\get_flag_value( $assoc_args, 'free-memory-on', 50 );
$sleep = \WP_CLI\Utils\get_flag_value( $assoc_args, 'pause', 0 );
$force = \WP_CLI\Utils\get_flag_value( $assoc_args, 'force', false );
ActionScheduler_DataController::set_free_ticks( $free_on );
ActionScheduler_DataController::set_sleep_time( $sleep );
$batches_completed = 0;
$actions_completed = 0;
$unlimited = $batches === 0;
if ( is_callable( [ ActionScheduler::store(), 'set_claim_filter' ] ) ) {
$exclude_groups = $this->parse_comma_separated_string( $exclude_groups );
if ( ! empty( $exclude_groups ) ) {
ActionScheduler::store()->set_claim_filter('exclude-groups', $exclude_groups );
}
}
try {
// Custom queue cleaner instance.
$cleaner = new ActionScheduler_QueueCleaner( null, $clean );
// Get the queue runner instance
$runner = new ActionScheduler_WPCLI_QueueRunner( null, null, $cleaner );
// Determine how many tasks will be run in the first batch.
$total = $runner->setup( $batch, $hooks, $group, $force );
// Run actions for as long as possible.
while ( $total > 0 ) {
$this->print_total_actions( $total );
$actions_completed += $runner->run();
$batches_completed++;
// Maybe set up tasks for the next batch.
$total = ( $unlimited || $batches_completed < $batches ) ? $runner->setup( $batch, $hooks, $group, $force ) : 0;
}
} catch ( Exception $e ) {
$this->print_error( $e );
}
$this->print_total_batches( $batches_completed );
$this->print_success( $actions_completed );
}
/**
* Converts a string of comma-separated values into an array of those same values.
*
* @param string $string The string of one or more comma separated values.
*
* @return array
*/
private function parse_comma_separated_string( $string ): array {
return array_filter( str_getcsv( $string ) );
}
/**
* Print WP CLI message about how many actions are about to be processed.
*
* @author Jeremy Pry
*
* @param int $total
*/
protected function print_total_actions( $total ) {
WP_CLI::log(
sprintf(
/* translators: %d refers to how many scheduled tasks were found to run */
_n( 'Found %d scheduled task', 'Found %d scheduled tasks', $total, 'woocommerce' ),
$total
)
);
}
/**
* Print WP CLI message about how many batches of actions were processed.
*
* @author Jeremy Pry
*
* @param int $batches_completed
*/
protected function print_total_batches( $batches_completed ) {
WP_CLI::log(
sprintf(
/* translators: %d refers to the total number of batches executed */
_n( '%d batch executed.', '%d batches executed.', $batches_completed, 'woocommerce' ),
$batches_completed
)
);
}
/**
* Convert an exception into a WP CLI error.
*
* @author Jeremy Pry
*
* @param Exception $e The error object.
*
* @throws \WP_CLI\ExitException
*/
protected function print_error( Exception $e ) {
WP_CLI::error(
sprintf(
/* translators: %s refers to the exception error message */
__( 'There was an error running the action scheduler: %s', 'woocommerce' ),
$e->getMessage()
)
);
}
/**
* Print a success message with the number of completed actions.
*
* @author Jeremy Pry
*
* @param int $actions_completed
*/
protected function print_success( $actions_completed ) {
WP_CLI::success(
sprintf(
/* translators: %d refers to the total number of tasks completed */
_n( '%d scheduled task completed.', '%d scheduled tasks completed.', $actions_completed, 'woocommerce' ),
$actions_completed
)
);
}
}

View File

@@ -0,0 +1,149 @@
<?php
// phpcs:ignoreFile
namespace Action_Scheduler\WP_CLI;
use Action_Scheduler\Migration\Config;
use Action_Scheduler\Migration\Runner;
use Action_Scheduler\Migration\Scheduler;
use Action_Scheduler\Migration\Controller;
use WP_CLI;
use WP_CLI_Command;
/**
* Class Migration_Command
*
* @package Action_Scheduler\WP_CLI
*
* @since 3.0.0
*
* @codeCoverageIgnore
*/
class Migration_Command extends WP_CLI_Command {
/** @var int */
private $total_processed = 0;
/**
* Register the command with WP-CLI
*/
public function register() {
if ( ! defined( 'WP_CLI' ) || ! WP_CLI ) {
return;
}
WP_CLI::add_command( 'action-scheduler migrate', [ $this, 'migrate' ], [
'shortdesc' => 'Migrates actions to the DB tables store',
'synopsis' => [
[
'type' => 'assoc',
'name' => 'batch-size',
'optional' => true,
'default' => 100,
'description' => 'The number of actions to process in each batch',
],
[
'type' => 'assoc',
'name' => 'free-memory-on',
'optional' => true,
'default' => 50,
'description' => 'The number of actions to process between freeing memory. 0 disables freeing memory',
],
[
'type' => 'assoc',
'name' => 'pause',
'optional' => true,
'default' => 0,
'description' => 'The number of seconds to pause when freeing memory',
],
[
'type' => 'flag',
'name' => 'dry-run',
'optional' => true,
'description' => 'Reports on the actions that would have been migrated, but does not change any data',
],
],
] );
}
/**
* Process the data migration.
*
* @param array $positional_args Required for WP CLI. Not used in migration.
* @param array $assoc_args Optional arguments.
*
* @return void
*/
public function migrate( $positional_args, $assoc_args ) {
$this->init_logging();
$config = $this->get_migration_config( $assoc_args );
$runner = new Runner( $config );
$runner->init_destination();
$batch_size = isset( $assoc_args[ 'batch-size' ] ) ? (int) $assoc_args[ 'batch-size' ] : 100;
$free_on = isset( $assoc_args[ 'free-memory-on' ] ) ? (int) $assoc_args[ 'free-memory-on' ] : 50;
$sleep = isset( $assoc_args[ 'pause' ] ) ? (int) $assoc_args[ 'pause' ] : 0;
\ActionScheduler_DataController::set_free_ticks( $free_on );
\ActionScheduler_DataController::set_sleep_time( $sleep );
do {
$actions_processed = $runner->run( $batch_size );
$this->total_processed += $actions_processed;
} while ( $actions_processed > 0 );
if ( ! $config->get_dry_run() ) {
// let the scheduler know that there's nothing left to do
$scheduler = new Scheduler();
$scheduler->mark_complete();
}
WP_CLI::success( sprintf( '%s complete. %d actions processed.', $config->get_dry_run() ? 'Dry run' : 'Migration', $this->total_processed ) );
}
/**
* Build the config object used to create the Runner
*
* @param array $args Optional arguments.
*
* @return ActionScheduler\Migration\Config
*/
private function get_migration_config( $args ) {
$args = wp_parse_args( $args, [
'dry-run' => false,
] );
$config = Controller::instance()->get_migration_config_object();
$config->set_dry_run( ! empty( $args[ 'dry-run' ] ) );
return $config;
}
/**
* Hook command line logging into migration actions.
*/
private function init_logging() {
add_action( 'action_scheduler/migrate_action_dry_run', function ( $action_id ) {
WP_CLI::debug( sprintf( 'Dry-run: migrated action %d', $action_id ) );
}, 10, 1 );
add_action( 'action_scheduler/no_action_to_migrate', function ( $action_id ) {
WP_CLI::debug( sprintf( 'No action found to migrate for ID %d', $action_id ) );
}, 10, 1 );
add_action( 'action_scheduler/migrate_action_failed', function ( $action_id ) {
WP_CLI::warning( sprintf( 'Failed migrating action with ID %d', $action_id ) );
}, 10, 1 );
add_action( 'action_scheduler/migrate_action_incomplete', function ( $source_id, $destination_id ) {
WP_CLI::warning( sprintf( 'Unable to remove source action with ID %d after migrating to new ID %d', $source_id, $destination_id ) );
}, 10, 2 );
add_action( 'action_scheduler/migrated_action', function ( $source_id, $destination_id ) {
WP_CLI::debug( sprintf( 'Migrated source action with ID %d to new store with ID %d', $source_id, $destination_id ) );
}, 10, 2 );
add_action( 'action_scheduler/migration_batch_starting', function ( $batch ) {
WP_CLI::debug( 'Beginning migration of batch: ' . print_r( $batch, true ) );
}, 10, 1 );
add_action( 'action_scheduler/migration_batch_complete', function ( $batch ) {
WP_CLI::log( sprintf( 'Completed migration of %d actions', count( $batch ) ) );
}, 10, 1 );
}
}

View File

@@ -0,0 +1,120 @@
<?php
// phpcs:ignoreFile
namespace Action_Scheduler\WP_CLI;
/**
* WP_CLI progress bar for Action Scheduler.
*/
/**
* Class ProgressBar
*
* @package Action_Scheduler\WP_CLI
*
* @since 3.0.0
*
* @codeCoverageIgnore
*/
class ProgressBar {
/** @var integer */
protected $total_ticks;
/** @var integer */
protected $count;
/** @var integer */
protected $interval;
/** @var string */
protected $message;
/** @var \cli\progress\Bar */
protected $progress_bar;
/**
* ProgressBar constructor.
*
* @param string $message Text to display before the progress bar.
* @param integer $count Total number of ticks to be performed.
* @param integer $interval Optional. The interval in milliseconds between updates. Default 100.
*
* @throws Exception When this is not run within WP CLI
*/
public function __construct( $message, $count, $interval = 100 ) {
if ( ! ( defined( 'WP_CLI' ) && WP_CLI ) ) {
/* translators: %s php class name */
throw new \Exception( sprintf( __( 'The %s class can only be run within WP CLI.', 'woocommerce' ), __CLASS__ ) );
}
$this->total_ticks = 0;
$this->message = $message;
$this->count = $count;
$this->interval = $interval;
}
/**
* Increment the progress bar ticks.
*/
public function tick() {
if ( null === $this->progress_bar ) {
$this->setup_progress_bar();
}
$this->progress_bar->tick();
$this->total_ticks++;
do_action( 'action_scheduler/progress_tick', $this->total_ticks );
}
/**
* Get the progress bar tick count.
*
* @return int
*/
public function current() {
return $this->progress_bar ? $this->progress_bar->current() : 0;
}
/**
* Finish the current progress bar.
*/
public function finish() {
if ( null !== $this->progress_bar ) {
$this->progress_bar->finish();
}
$this->progress_bar = null;
}
/**
* Set the message used when creating the progress bar.
*
* @param string $message The message to be used when the next progress bar is created.
*/
public function set_message( $message ) {
$this->message = $message;
}
/**
* Set the count for a new progress bar.
*
* @param integer $count The total number of ticks expected to complete.
*/
public function set_count( $count ) {
$this->count = $count;
$this->finish();
}
/**
* Set up the progress bar.
*/
protected function setup_progress_bar() {
$this->progress_bar = \WP_CLI\Utils\make_progress_bar(
$this->message,
$this->count,
$this->interval
);
}
}

View File

@@ -0,0 +1,338 @@
<?php
// phpcs:ignoreFile
use Action_Scheduler\WP_CLI\Migration_Command;
use Action_Scheduler\Migration\Controller;
/**
* Class ActionScheduler
* @codeCoverageIgnore
*/
abstract class ActionScheduler {
private static $plugin_file = '';
/** @var ActionScheduler_ActionFactory */
private static $factory = NULL;
/** @var bool */
private static $data_store_initialized = false;
public static function factory() {
if ( !isset(self::$factory) ) {
self::$factory = new ActionScheduler_ActionFactory();
}
return self::$factory;
}
public static function store() {
return ActionScheduler_Store::instance();
}
public static function lock() {
return ActionScheduler_Lock::instance();
}
public static function logger() {
return ActionScheduler_Logger::instance();
}
public static function runner() {
return ActionScheduler_QueueRunner::instance();
}
public static function admin_view() {
return ActionScheduler_AdminView::instance();
}
/**
* Get the absolute system path to the plugin directory, or a file therein
* @static
* @param string $path
* @return string
*/
public static function plugin_path( $path ) {
$base = dirname(self::$plugin_file);
if ( $path ) {
return trailingslashit($base).$path;
} else {
return untrailingslashit($base);
}
}
/**
* Get the absolute URL to the plugin directory, or a file therein
* @static
* @param string $path
* @return string
*/
public static function plugin_url( $path ) {
return plugins_url($path, self::$plugin_file);
}
public static function autoload( $class ) {
$d = DIRECTORY_SEPARATOR;
$classes_dir = self::plugin_path( 'classes' . $d );
$separator = strrpos( $class, '\\' );
if ( false !== $separator ) {
if ( 0 !== strpos( $class, 'Action_Scheduler' ) ) {
return;
}
$class = substr( $class, $separator + 1 );
}
if ( 'Deprecated' === substr( $class, -10 ) ) {
$dir = self::plugin_path( 'deprecated' . $d );
} elseif ( self::is_class_abstract( $class ) ) {
$dir = $classes_dir . 'abstracts' . $d;
} elseif ( self::is_class_migration( $class ) ) {
$dir = $classes_dir . 'migration' . $d;
} elseif ( 'Schedule' === substr( $class, -8 ) ) {
$dir = $classes_dir . 'schedules' . $d;
} elseif ( 'Action' === substr( $class, -6 ) ) {
$dir = $classes_dir . 'actions' . $d;
} elseif ( 'Schema' === substr( $class, -6 ) ) {
$dir = $classes_dir . 'schema' . $d;
} elseif ( strpos( $class, 'ActionScheduler' ) === 0 ) {
$segments = explode( '_', $class );
$type = isset( $segments[ 1 ] ) ? $segments[ 1 ] : '';
switch ( $type ) {
case 'WPCLI':
$dir = $classes_dir . 'WP_CLI' . $d;
break;
case 'DBLogger':
case 'DBStore':
case 'HybridStore':
case 'wpPostStore':
case 'wpCommentLogger':
$dir = $classes_dir . 'data-stores' . $d;
break;
default:
$dir = $classes_dir;
break;
}
} elseif ( self::is_class_cli( $class ) ) {
$dir = $classes_dir . 'WP_CLI' . $d;
} elseif ( strpos( $class, 'CronExpression' ) === 0 ) {
$dir = self::plugin_path( 'lib' . $d . 'cron-expression' . $d );
} elseif ( strpos( $class, 'WP_Async_Request' ) === 0 ) {
$dir = self::plugin_path( 'lib' . $d );
} else {
return;
}
if ( file_exists( $dir . "{$class}.php" ) ) {
include( $dir . "{$class}.php" );
return;
}
}
/**
* Initialize the plugin
*
* @static
* @param string $plugin_file
*/
public static function init( $plugin_file ) {
self::$plugin_file = $plugin_file;
spl_autoload_register( array( __CLASS__, 'autoload' ) );
/**
* Fires in the early stages of Action Scheduler init hook.
*/
do_action( 'action_scheduler_pre_init' );
require_once( self::plugin_path( 'functions.php' ) );
ActionScheduler_DataController::init();
$store = self::store();
$logger = self::logger();
$runner = self::runner();
$admin_view = self::admin_view();
// Ensure initialization on plugin activation.
if ( ! did_action( 'init' ) ) {
add_action( 'init', array( $admin_view, 'init' ), 0, 0 ); // run before $store::init()
add_action( 'init', array( $store, 'init' ), 1, 0 );
add_action( 'init', array( $logger, 'init' ), 1, 0 );
add_action( 'init', array( $runner, 'init' ), 1, 0 );
add_action(
'init',
/**
* Runs after the active store's init() method has been called.
*
* It would probably be preferable to have $store->init() (or it's parent method) set this itself,
* once it has initialized, however that would cause problems in cases where a custom data store is in
* use and it has not yet been updated to follow that same logic.
*/
function () {
self::$data_store_initialized = true;
/**
* Fires when Action Scheduler is ready: it is safe to use the procedural API after this point.
*
* @since 3.5.5
*/
do_action( 'action_scheduler_init' );
},
1
);
} else {
$admin_view->init();
$store->init();
$logger->init();
$runner->init();
self::$data_store_initialized = true;
/**
* Fires when Action Scheduler is ready: it is safe to use the procedural API after this point.
*
* @since 3.5.5
*/
do_action( 'action_scheduler_init' );
}
if ( apply_filters( 'action_scheduler_load_deprecated_functions', true ) ) {
require_once( self::plugin_path( 'deprecated/functions.php' ) );
}
if ( defined( 'WP_CLI' ) && WP_CLI ) {
WP_CLI::add_command( 'action-scheduler', 'ActionScheduler_WPCLI_Scheduler_command' );
WP_CLI::add_command( 'action-scheduler', 'ActionScheduler_WPCLI_Clean_Command' );
if ( ! ActionScheduler_DataController::is_migration_complete() && Controller::instance()->allow_migration() ) {
$command = new Migration_Command();
$command->register();
}
}
/**
* Handle WP comment cleanup after migration.
*/
if ( is_a( $logger, 'ActionScheduler_DBLogger' ) && ActionScheduler_DataController::is_migration_complete() && ActionScheduler_WPCommentCleaner::has_logs() ) {
ActionScheduler_WPCommentCleaner::init();
}
add_action( 'action_scheduler/migration_complete', 'ActionScheduler_WPCommentCleaner::maybe_schedule_cleanup' );
}
/**
* Check whether the AS data store has been initialized.
*
* @param string $function_name The name of the function being called. Optional. Default `null`.
* @return bool
*/
public static function is_initialized( $function_name = null ) {
if ( ! self::$data_store_initialized && ! empty( $function_name ) ) {
$message = sprintf(
/* translators: %s function name. */
__( '%s() was called before the Action Scheduler data store was initialized', 'woocommerce' ),
esc_attr( $function_name )
);
_doing_it_wrong( $function_name, $message, '3.1.6' );
}
return self::$data_store_initialized;
}
/**
* Determine if the class is one of our abstract classes.
*
* @since 3.0.0
*
* @param string $class The class name.
*
* @return bool
*/
protected static function is_class_abstract( $class ) {
static $abstracts = array(
'ActionScheduler' => true,
'ActionScheduler_Abstract_ListTable' => true,
'ActionScheduler_Abstract_QueueRunner' => true,
'ActionScheduler_Abstract_Schedule' => true,
'ActionScheduler_Abstract_RecurringSchedule' => true,
'ActionScheduler_Lock' => true,
'ActionScheduler_Logger' => true,
'ActionScheduler_Abstract_Schema' => true,
'ActionScheduler_Store' => true,
'ActionScheduler_TimezoneHelper' => true,
);
return isset( $abstracts[ $class ] ) && $abstracts[ $class ];
}
/**
* Determine if the class is one of our migration classes.
*
* @since 3.0.0
*
* @param string $class The class name.
*
* @return bool
*/
protected static function is_class_migration( $class ) {
static $migration_segments = array(
'ActionMigrator' => true,
'BatchFetcher' => true,
'DBStoreMigrator' => true,
'DryRun' => true,
'LogMigrator' => true,
'Config' => true,
'Controller' => true,
'Runner' => true,
'Scheduler' => true,
);
$segments = explode( '_', $class );
$segment = isset( $segments[ 1 ] ) ? $segments[ 1 ] : $class;
return isset( $migration_segments[ $segment ] ) && $migration_segments[ $segment ];
}
/**
* Determine if the class is one of our WP CLI classes.
*
* @since 3.0.0
*
* @param string $class The class name.
*
* @return bool
*/
protected static function is_class_cli( $class ) {
static $cli_segments = array(
'QueueRunner' => true,
'Command' => true,
'ProgressBar' => true,
);
$segments = explode( '_', $class );
$segment = isset( $segments[ 1 ] ) ? $segments[ 1 ] : $class;
return isset( $cli_segments[ $segment ] ) && $cli_segments[ $segment ];
}
final public function __clone() {
trigger_error("Singleton. No cloning allowed!", E_USER_ERROR);
}
final public function __wakeup() {
trigger_error("Singleton. No serialization allowed!", E_USER_ERROR);
}
final private function __construct() {}
/** Deprecated **/
public static function get_datetime_object( $when = null, $timezone = 'UTC' ) {
_deprecated_function( __METHOD__, '2.0', 'wcs_add_months()' );
return as_get_datetime_object( $when, $timezone );
}
/**
* Issue deprecated warning if an Action Scheduler function is called in the shutdown hook.
*
* @param string $function_name The name of the function being called.
* @deprecated 3.1.6.
*/
public static function check_shutdown_hook( $function_name ) {
_deprecated_function( __FUNCTION__, '3.1.6' );
}
}

View File

@@ -0,0 +1,777 @@
<?php
// phpcs:ignoreFile
if ( ! class_exists( 'WP_List_Table' ) ) {
require_once ABSPATH . 'wp-admin/includes/class-wp-list-table.php';
}
/**
* Action Scheduler Abstract List Table class
*
* This abstract class enhances WP_List_Table making it ready to use.
*
* By extending this class we can focus on describing how our table looks like,
* which columns needs to be shown, filter, ordered by and more and forget about the details.
*
* This class supports:
* - Bulk actions
* - Search
* - Sortable columns
* - Automatic translations of the columns
*
* @codeCoverageIgnore
* @since 2.0.0
*/
abstract class ActionScheduler_Abstract_ListTable extends WP_List_Table {
/**
* The table name
*
* @var string
*/
protected $table_name;
/**
* Package name, used to get options from WP_List_Table::get_items_per_page.
*
* @var string
*/
protected $package;
/**
* How many items do we render per page?
*
* @var int
*/
protected $items_per_page = 10;
/**
* Enables search in this table listing. If this array
* is empty it means the listing is not searchable.
*
* @var array
*/
protected $search_by = array();
/**
* Columns to show in the table listing. It is a key => value pair. The
* key must much the table column name and the value is the label, which is
* automatically translated.
*
* @var array
*/
protected $columns = array();
/**
* Defines the row-actions. It expects an array where the key
* is the column name and the value is an array of actions.
*
* The array of actions are key => value, where key is the method name
* (with the prefix row_action_<key>) and the value is the label
* and title.
*
* @var array
*/
protected $row_actions = array();
/**
* The Primary key of our table
*
* @var string
*/
protected $ID = 'ID';
/**
* Enables sorting, it expects an array
* of columns (the column names are the values)
*
* @var array
*/
protected $sort_by = array();
/**
* The default sort order
*
* @var string
*/
protected $filter_by = array();
/**
* The status name => count combinations for this table's items. Used to display status filters.
*
* @var array
*/
protected $status_counts = array();
/**
* Notices to display when loading the table. Array of arrays of form array( 'class' => {updated|error}, 'message' => 'This is the notice text display.' ).
*
* @var array
*/
protected $admin_notices = array();
/**
* Localised string displayed in the <h1> element above the able.
*
* @var string
*/
protected $table_header;
/**
* Enables bulk actions. It must be an array where the key is the action name
* and the value is the label (which is translated automatically). It is important
* to notice that it will check that the method exists (`bulk_$name`) and will throw
* an exception if it does not exists.
*
* This class will automatically check if the current request has a bulk action, will do the
* validations and afterwards will execute the bulk method, with two arguments. The first argument
* is the array with primary keys, the second argument is a string with a list of the primary keys,
* escaped and ready to use (with `IN`).
*
* @var array
*/
protected $bulk_actions = array();
/**
* Makes translation easier, it basically just wraps
* `_x` with some default (the package name).
*
* @param string $text The new text to translate.
* @param string $context The context of the text.
* @return string|void The translated text.
*
* @deprecated 3.0.0 Use `_x()` instead.
*/
protected function translate( $text, $context = '' ) {
return $text;
}
/**
* Reads `$this->bulk_actions` and returns an array that WP_List_Table understands. It
* also validates that the bulk method handler exists. It throws an exception because
* this is a library meant for developers and missing a bulk method is a development-time error.
*
* @return array
*
* @throws RuntimeException Throws RuntimeException when the bulk action does not have a callback method.
*/
protected function get_bulk_actions() {
$actions = array();
foreach ( $this->bulk_actions as $action => $label ) {
if ( ! is_callable( array( $this, 'bulk_' . $action ) ) ) {
throw new RuntimeException( "The bulk action $action does not have a callback method" );
}
$actions[ $action ] = $label;
}
return $actions;
}
/**
* Checks if the current request has a bulk action. If that is the case it will validate and will
* execute the bulk method handler. Regardless if the action is valid or not it will redirect to
* the previous page removing the current arguments that makes this request a bulk action.
*/
protected function process_bulk_action() {
global $wpdb;
// Detect when a bulk action is being triggered.
$action = $this->current_action();
if ( ! $action ) {
return;
}
check_admin_referer( 'bulk-' . $this->_args['plural'] );
$method = 'bulk_' . $action;
if ( array_key_exists( $action, $this->bulk_actions ) && is_callable( array( $this, $method ) ) && ! empty( $_GET['ID'] ) && is_array( $_GET['ID'] ) ) {
$ids_sql = '(' . implode( ',', array_fill( 0, count( $_GET['ID'] ), '%s' ) ) . ')';
$id = array_map( 'absint', $_GET['ID'] );
$this->$method( $id, $wpdb->prepare( $ids_sql, $id ) ); //phpcs:ignore WordPress.DB.PreparedSQL
}
if ( isset( $_SERVER['REQUEST_URI'] ) ) {
wp_safe_redirect(
remove_query_arg(
array( '_wp_http_referer', '_wpnonce', 'ID', 'action', 'action2' ),
esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ) )
)
);
exit;
}
}
/**
* Default code for deleting entries.
* validated already by process_bulk_action()
*
* @param array $ids ids of the items to delete.
* @param string $ids_sql the sql for the ids.
* @return void
*/
protected function bulk_delete( array $ids, $ids_sql ) {
$store = ActionScheduler::store();
foreach ( $ids as $action_id ) {
$store->delete( $action_id );
}
}
/**
* Prepares the _column_headers property which is used by WP_Table_List at rendering.
* It merges the columns and the sortable columns.
*/
protected function prepare_column_headers() {
$this->_column_headers = array(
$this->get_columns(),
get_hidden_columns( $this->screen ),
$this->get_sortable_columns(),
);
}
/**
* Reads $this->sort_by and returns the columns name in a format that WP_Table_List
* expects
*/
public function get_sortable_columns() {
$sort_by = array();
foreach ( $this->sort_by as $column ) {
$sort_by[ $column ] = array( $column, true );
}
return $sort_by;
}
/**
* Returns the columns names for rendering. It adds a checkbox for selecting everything
* as the first column
*/
public function get_columns() {
$columns = array_merge(
array( 'cb' => '<input type="checkbox" />' ),
$this->columns
);
return $columns;
}
/**
* Get prepared LIMIT clause for items query
*
* @global wpdb $wpdb
*
* @return string Prepared LIMIT clause for items query.
*/
protected function get_items_query_limit() {
global $wpdb;
$per_page = $this->get_items_per_page( $this->get_per_page_option_name(), $this->items_per_page );
return $wpdb->prepare( 'LIMIT %d', $per_page );
}
/**
* Returns the number of items to offset/skip for this current view.
*
* @return int
*/
protected function get_items_offset() {
$per_page = $this->get_items_per_page( $this->get_per_page_option_name(), $this->items_per_page );
$current_page = $this->get_pagenum();
if ( 1 < $current_page ) {
$offset = $per_page * ( $current_page - 1 );
} else {
$offset = 0;
}
return $offset;
}
/**
* Get prepared OFFSET clause for items query
*
* @global wpdb $wpdb
*
* @return string Prepared OFFSET clause for items query.
*/
protected function get_items_query_offset() {
global $wpdb;
return $wpdb->prepare( 'OFFSET %d', $this->get_items_offset() );
}
/**
* Prepares the ORDER BY sql statement. It uses `$this->sort_by` to know which
* columns are sortable. This requests validates the orderby $_GET parameter is a valid
* column and sortable. It will also use order (ASC|DESC) using DESC by default.
*/
protected function get_items_query_order() {
if ( empty( $this->sort_by ) ) {
return '';
}
$orderby = esc_sql( $this->get_request_orderby() );
$order = esc_sql( $this->get_request_order() );
return "ORDER BY {$orderby} {$order}";
}
/**
* Return the sortable column specified for this request to order the results by, if any.
*
* @return string
*/
protected function get_request_orderby() {
$valid_sortable_columns = array_values( $this->sort_by );
if ( ! empty( $_GET['orderby'] ) && in_array( $_GET['orderby'], $valid_sortable_columns, true ) ) { //phpcs:ignore WordPress.Security.NonceVerification.Recommended
$orderby = sanitize_text_field( wp_unslash( $_GET['orderby'] ) ); //phpcs:ignore WordPress.Security.NonceVerification.Recommended
} else {
$orderby = $valid_sortable_columns[0];
}
return $orderby;
}
/**
* Return the sortable column order specified for this request.
*
* @return string
*/
protected function get_request_order() {
if ( ! empty( $_GET['order'] ) && 'desc' === strtolower( sanitize_text_field( wp_unslash( $_GET['order'] ) ) ) ) { //phpcs:ignore WordPress.Security.NonceVerification.Recommended
$order = 'DESC';
} else {
$order = 'ASC';
}
return $order;
}
/**
* Return the status filter for this request, if any.
*
* @return string
*/
protected function get_request_status() {
$status = ( ! empty( $_GET['status'] ) ) ? sanitize_text_field( wp_unslash( $_GET['status'] ) ) : ''; //phpcs:ignore WordPress.Security.NonceVerification.Recommended
return $status;
}
/**
* Return the search filter for this request, if any.
*
* @return string
*/
protected function get_request_search_query() {
$search_query = ( ! empty( $_GET['s'] ) ) ? sanitize_text_field( wp_unslash( $_GET['s'] ) ) : ''; //phpcs:ignore WordPress.Security.NonceVerification.Recommended
return $search_query;
}
/**
* Process and return the columns name. This is meant for using with SQL, this means it
* always includes the primary key.
*
* @return array
*/
protected function get_table_columns() {
$columns = array_keys( $this->columns );
if ( ! in_array( $this->ID, $columns, true ) ) {
$columns[] = $this->ID;
}
return $columns;
}
/**
* Check if the current request is doing a "full text" search. If that is the case
* prepares the SQL to search texts using LIKE.
*
* If the current request does not have any search or if this list table does not support
* that feature it will return an empty string.
*
* @return string
*/
protected function get_items_query_search() {
global $wpdb;
if ( empty( $_GET['s'] ) || empty( $this->search_by ) ) { //phpcs:ignore WordPress.Security.NonceVerification.Recommended
return '';
}
$search_string = sanitize_text_field( wp_unslash( $_GET['s'] ) ); //phpcs:ignore WordPress.Security.NonceVerification.Recommended
$filter = array();
foreach ( $this->search_by as $column ) {
$wild = '%';
$sql_like = $wild . $wpdb->esc_like( $search_string ) . $wild;
$filter[] = $wpdb->prepare( '`' . $column . '` LIKE %s', $sql_like ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.DB.PreparedSQL.NotPrepared
}
return implode( ' OR ', $filter );
}
/**
* Prepares the SQL to filter rows by the options defined at `$this->filter_by`. Before trusting
* any data sent by the user it validates that it is a valid option.
*/
protected function get_items_query_filters() {
global $wpdb;
if ( ! $this->filter_by || empty( $_GET['filter_by'] ) || ! is_array( $_GET['filter_by'] ) ) { //phpcs:ignore WordPress.Security.NonceVerification.Recommended
return '';
}
$filter = array();
foreach ( $this->filter_by as $column => $options ) {
if ( empty( $_GET['filter_by'][ $column ] ) || empty( $options[ $_GET['filter_by'][ $column ] ] ) ) { //phpcs:ignore WordPress.Security.NonceVerification.Recommended
continue;
}
$filter[] = $wpdb->prepare( "`$column` = %s", sanitize_text_field( wp_unslash( $_GET['filter_by'][ $column ] ) ) ); //phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
}
return implode( ' AND ', $filter );
}
/**
* Prepares the data to feed WP_Table_List.
*
* This has the core for selecting, sorting and filting data. To keep the code simple
* its logic is split among many methods (get_items_query_*).
*
* Beside populating the items this function will also count all the records that matches
* the filtering criteria and will do fill the pagination variables.
*/
public function prepare_items() {
global $wpdb;
$this->process_bulk_action();
$this->process_row_actions();
if ( ! empty( $_REQUEST['_wp_http_referer'] && ! empty( $_SERVER['REQUEST_URI'] ) ) ) { //phpcs:ignore WordPress.Security.NonceVerification.Recommended
// _wp_http_referer is used only on bulk actions, we remove it to keep the $_GET shorter
wp_safe_redirect( remove_query_arg( array( '_wp_http_referer', '_wpnonce' ), esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ) ) ) );
exit;
}
$this->prepare_column_headers();
$limit = $this->get_items_query_limit();
$offset = $this->get_items_query_offset();
$order = $this->get_items_query_order();
$where = array_filter(
array(
$this->get_items_query_search(),
$this->get_items_query_filters(),
)
);
$columns = '`' . implode( '`, `', $this->get_table_columns() ) . '`';
if ( ! empty( $where ) ) {
$where = 'WHERE (' . implode( ') AND (', $where ) . ')';
} else {
$where = '';
}
$sql = "SELECT $columns FROM {$this->table_name} {$where} {$order} {$limit} {$offset}";
$this->set_items( $wpdb->get_results( $sql, ARRAY_A ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
$query_count = "SELECT COUNT({$this->ID}) FROM {$this->table_name} {$where}";
$total_items = $wpdb->get_var( $query_count ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
$per_page = $this->get_items_per_page( $this->get_per_page_option_name(), $this->items_per_page );
$this->set_pagination_args(
array(
'total_items' => $total_items,
'per_page' => $per_page,
'total_pages' => ceil( $total_items / $per_page ),
)
);
}
/**
* Display the table.
*
* @param string $which The name of the table.
*/
public function extra_tablenav( $which ) {
if ( ! $this->filter_by || 'top' !== $which ) {
return;
}
echo '<div class="alignleft actions">';
foreach ( $this->filter_by as $id => $options ) {
$default = ! empty( $_GET['filter_by'][ $id ] ) ? sanitize_text_field( wp_unslash( $_GET['filter_by'][ $id ] ) ) : ''; //phpcs:ignore WordPress.Security.NonceVerification.Recommended
if ( empty( $options[ $default ] ) ) {
$default = '';
}
echo '<select name="filter_by[' . esc_attr( $id ) . ']" class="first" id="filter-by-' . esc_attr( $id ) . '">';
foreach ( $options as $value => $label ) {
echo '<option value="' . esc_attr( $value ) . '" ' . esc_html( $value === $default ? 'selected' : '' ) . '>'
. esc_html( $label )
. '</option>';
}
echo '</select>';
}
submit_button( esc_html__( 'Filter', 'woocommerce' ), '', 'filter_action', false, array( 'id' => 'post-query-submit' ) );
echo '</div>';
}
/**
* Set the data for displaying. It will attempt to unserialize (There is a chance that some columns
* are serialized). This can be override in child classes for futher data transformation.
*
* @param array $items Items array.
*/
protected function set_items( array $items ) {
$this->items = array();
foreach ( $items as $item ) {
$this->items[ $item[ $this->ID ] ] = array_map( 'maybe_unserialize', $item );
}
}
/**
* Renders the checkbox for each row, this is the first column and it is named ID regardless
* of how the primary key is named (to keep the code simpler). The bulk actions will do the proper
* name transformation though using `$this->ID`.
*
* @param array $row The row to render.
*/
public function column_cb( $row ) {
return '<input name="ID[]" type="checkbox" value="' . esc_attr( $row[ $this->ID ] ) . '" />';
}
/**
* Renders the row-actions.
*
* This method renders the action menu, it reads the definition from the $row_actions property,
* and it checks that the row action method exists before rendering it.
*
* @param array $row Row to be rendered.
* @param string $column_name Column name.
* @return string
*/
protected function maybe_render_actions( $row, $column_name ) {
if ( empty( $this->row_actions[ $column_name ] ) ) {
return;
}
$row_id = $row[ $this->ID ];
$actions = '<div class="row-actions">';
$action_count = 0;
foreach ( $this->row_actions[ $column_name ] as $action_key => $action ) {
$action_count++;
if ( ! method_exists( $this, 'row_action_' . $action_key ) ) {
continue;
}
$action_link = ! empty( $action['link'] ) ? $action['link'] : add_query_arg(
array(
'row_action' => $action_key,
'row_id' => $row_id,
'nonce' => wp_create_nonce( $action_key . '::' . $row_id ),
)
);
$span_class = ! empty( $action['class'] ) ? $action['class'] : $action_key;
$separator = ( $action_count < count( $this->row_actions[ $column_name ] ) ) ? ' | ' : '';
$actions .= sprintf( '<span class="%s">', esc_attr( $span_class ) );
$actions .= sprintf( '<a href="%1$s" title="%2$s">%3$s</a>', esc_url( $action_link ), esc_attr( $action['desc'] ), esc_html( $action['name'] ) );
$actions .= sprintf( '%s</span>', $separator );
}
$actions .= '</div>';
return $actions;
}
/**
* Process the bulk actions.
*
* @return void
*/
protected function process_row_actions() {
$parameters = array( 'row_action', 'row_id', 'nonce' );
foreach ( $parameters as $parameter ) {
if ( empty( $_REQUEST[ $parameter ] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
return;
}
}
$action = sanitize_text_field( wp_unslash( $_REQUEST['row_action'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotValidated
$row_id = sanitize_text_field( wp_unslash( $_REQUEST['row_id'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotValidated
$nonce = sanitize_text_field( wp_unslash( $_REQUEST['nonce'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotValidated
$method = 'row_action_' . $action; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
if ( wp_verify_nonce( $nonce, $action . '::' . $row_id ) && method_exists( $this, $method ) ) {
$this->$method( sanitize_text_field( wp_unslash( $row_id ) ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
}
if ( isset( $_SERVER['REQUEST_URI'] ) ) {
wp_safe_redirect(
remove_query_arg(
array( 'row_id', 'row_action', 'nonce' ),
esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ) )
)
);
exit;
}
}
/**
* Default column formatting, it will escape everythig for security.
*
* @param array $item The item array.
* @param string $column_name Column name to display.
*
* @return string
*/
public function column_default( $item, $column_name ) {
$column_html = esc_html( $item[ $column_name ] );
$column_html .= $this->maybe_render_actions( $item, $column_name );
return $column_html;
}
/**
* Display the table heading and search query, if any
*/
protected function display_header() {
echo '<h1 class="wp-heading-inline">' . esc_attr( $this->table_header ) . '</h1>';
if ( $this->get_request_search_query() ) {
/* translators: %s: search query */
echo '<span class="subtitle">' . esc_attr( sprintf( __( 'Search results for "%s"', 'woocommerce' ), $this->get_request_search_query() ) ) . '</span>';
}
echo '<hr class="wp-header-end">';
}
/**
* Display the table heading and search query, if any
*/
protected function display_admin_notices() {
foreach ( $this->admin_notices as $notice ) {
echo '<div id="message" class="' . esc_attr( $notice['class'] ) . '">';
echo ' <p>' . wp_kses_post( $notice['message'] ) . '</p>';
echo '</div>';
}
}
/**
* Prints the available statuses so the user can click to filter.
*/
protected function display_filter_by_status() {
$status_list_items = array();
$request_status = $this->get_request_status();
// Helper to set 'all' filter when not set on status counts passed in.
if ( ! isset( $this->status_counts['all'] ) ) {
$all_count = array_sum( $this->status_counts );
if ( isset( $this->status_counts['past-due'] ) ) {
$all_count -= $this->status_counts['past-due'];
}
$this->status_counts = array( 'all' => $all_count ) + $this->status_counts;
}
// Translated status labels.
$status_labels = ActionScheduler_Store::instance()->get_status_labels();
$status_labels['all'] = _x( 'All', 'status labels', 'woocommerce' );
$status_labels['past-due'] = _x( 'Past-due', 'status labels', 'woocommerce' );
foreach ( $this->status_counts as $status_slug => $count ) {
if ( 0 === $count ) {
continue;
}
if ( $status_slug === $request_status || ( empty( $request_status ) && 'all' === $status_slug ) ) {
$status_list_item = '<li class="%1$s"><a href="%2$s" class="current">%3$s</a> (%4$d)</li>';
} else {
$status_list_item = '<li class="%1$s"><a href="%2$s">%3$s</a> (%4$d)</li>';
}
$status_name = isset( $status_labels[ $status_slug ] ) ? $status_labels[ $status_slug ] : ucfirst( $status_slug );
$status_filter_url = ( 'all' === $status_slug ) ? remove_query_arg( 'status' ) : add_query_arg( 'status', $status_slug );
$status_filter_url = remove_query_arg( array( 'paged', 's' ), $status_filter_url );
$status_list_items[] = sprintf( $status_list_item, esc_attr( $status_slug ), esc_url( $status_filter_url ), esc_html( $status_name ), absint( $count ) );
}
if ( $status_list_items ) {
echo '<ul class="subsubsub">';
echo implode( " | \n", $status_list_items ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo '</ul>';
}
}
/**
* Renders the table list, we override the original class to render the table inside a form
* and to render any needed HTML (like the search box). By doing so the callee of a function can simple
* forget about any extra HTML.
*/
protected function display_table() {
echo '<form id="' . esc_attr( $this->_args['plural'] ) . '-filter" method="get">';
foreach ( $_GET as $key => $value ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
if ( '_' === $key[0] || 'paged' === $key || 'ID' === $key ) {
continue;
}
echo '<input type="hidden" name="' . esc_attr( $key ) . '" value="' . esc_attr( $value ) . '" />';
}
if ( ! empty( $this->search_by ) ) {
echo $this->search_box( $this->get_search_box_button_text(), 'plugin' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
parent::display();
echo '</form>';
}
/**
* Process any pending actions.
*/
public function process_actions() {
$this->process_bulk_action();
$this->process_row_actions();
if ( ! empty( $_REQUEST['_wp_http_referer'] ) && ! empty( $_SERVER['REQUEST_URI'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
// _wp_http_referer is used only on bulk actions, we remove it to keep the $_GET shorter
wp_safe_redirect( remove_query_arg( array( '_wp_http_referer', '_wpnonce' ), esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ) ) ) );
exit;
}
}
/**
* Render the list table page, including header, notices, status filters and table.
*/
public function display_page() {
$this->prepare_items();
echo '<div class="wrap">';
$this->display_header();
$this->display_admin_notices();
$this->display_filter_by_status();
$this->display_table();
echo '</div>';
}
/**
* Get the text to display in the search box on the list table.
*/
protected function get_search_box_placeholder() {
return esc_html__( 'Search', 'woocommerce' );
}
/**
* Gets the screen per_page option name.
*
* @return string
*/
protected function get_per_page_option_name() {
return $this->package . '_items_per_page';
}
}

View File

@@ -0,0 +1,373 @@
<?php
// phpcs:ignoreFile
/**
* Abstract class with common Queue Cleaner functionality.
*/
abstract class ActionScheduler_Abstract_QueueRunner extends ActionScheduler_Abstract_QueueRunner_Deprecated {
/** @var ActionScheduler_QueueCleaner */
protected $cleaner;
/** @var ActionScheduler_FatalErrorMonitor */
protected $monitor;
/** @var ActionScheduler_Store */
protected $store;
/**
* The created time.
*
* Represents when the queue runner was constructed and used when calculating how long a PHP request has been running.
* For this reason it should be as close as possible to the PHP request start time.
*
* @var int
*/
private $created_time;
/**
* ActionScheduler_Abstract_QueueRunner constructor.
*
* @param ActionScheduler_Store $store
* @param ActionScheduler_FatalErrorMonitor $monitor
* @param ActionScheduler_QueueCleaner $cleaner
*/
public function __construct( ActionScheduler_Store $store = null, ActionScheduler_FatalErrorMonitor $monitor = null, ActionScheduler_QueueCleaner $cleaner = null ) {
$this->created_time = microtime( true );
$this->store = $store ? $store : ActionScheduler_Store::instance();
$this->monitor = $monitor ? $monitor : new ActionScheduler_FatalErrorMonitor( $this->store );
$this->cleaner = $cleaner ? $cleaner : new ActionScheduler_QueueCleaner( $this->store );
}
/**
* Process an individual action.
*
* @param int $action_id The action ID to process.
* @param string $context Optional identifer for the context in which this action is being processed, e.g. 'WP CLI' or 'WP Cron'
* Generally, this should be capitalised and not localised as it's a proper noun.
*/
public function process_action( $action_id, $context = '' ) {
// Temporarily override the error handler while we process the current action.
set_error_handler(
/**
* Temporary error handler which can catch errors and convert them into exceptions. This faciliates more
* robust error handling across all supported PHP versions.
*
* @throws Exception
*
* @param int $type Error level expressed as an integer.
* @param string $message Error message.
*/
function ( $type, $message ) {
throw new Exception( $message );
},
E_USER_ERROR | E_RECOVERABLE_ERROR
);
/*
* The nested try/catch structure is required because we potentially need to convert thrown errors into
* exceptions (and an exception thrown from a catch block cannot be caught by a later catch block in the *same*
* structure).
*/
try {
try {
$valid_action = false;
do_action( 'action_scheduler_before_execute', $action_id, $context );
if ( ActionScheduler_Store::STATUS_PENDING !== $this->store->get_status( $action_id ) ) {
do_action( 'action_scheduler_execution_ignored', $action_id, $context );
return;
}
$valid_action = true;
do_action( 'action_scheduler_begin_execute', $action_id, $context );
$action = $this->store->fetch_action( $action_id );
$this->store->log_execution( $action_id );
$action->execute();
do_action( 'action_scheduler_after_execute', $action_id, $action, $context );
$this->store->mark_complete( $action_id );
} catch ( Throwable $e ) {
// Throwable is defined when executing under PHP 7.0 and up. We convert it to an exception, for
// compatibility with ActionScheduler_Logger.
throw new Exception( $e->getMessage(), $e->getCode(), $e );
}
} catch ( Exception $e ) {
// This catch block exists for compatibility with PHP 5.6.
$this->handle_action_error( $action_id, $e, $context, $valid_action );
} finally {
restore_error_handler();
}
if ( isset( $action ) && is_a( $action, 'ActionScheduler_Action' ) && $action->get_schedule()->is_recurring() ) {
$this->schedule_next_instance( $action, $action_id );
}
}
/**
* Marks actions as either having failed execution or failed validation, as appropriate.
*
* @param int $action_id Action ID.
* @param Exception $e Exception instance.
* @param string $context Execution context.
* @param bool $valid_action If the action is valid.
*
* @return void
*/
private function handle_action_error( $action_id, $e, $context, $valid_action ) {
if ( $valid_action ) {
$this->store->mark_failure( $action_id );
/**
* Runs when action execution fails.
*
* @param int $action_id Action ID.
* @param Exception $e Exception instance.
* @param string $context Execution context.
*/
do_action( 'action_scheduler_failed_execution', $action_id, $e, $context );
} else {
/**
* Runs when action validation fails.
*
* @param int $action_id Action ID.
* @param Exception $e Exception instance.
* @param string $context Execution context.
*/
do_action( 'action_scheduler_failed_validation', $action_id, $e, $context );
}
}
/**
* Schedule the next instance of the action if necessary.
*
* @param ActionScheduler_Action $action
* @param int $action_id
*/
protected function schedule_next_instance( ActionScheduler_Action $action, $action_id ) {
// If a recurring action has been consistently failing, we may wish to stop rescheduling it.
if (
ActionScheduler_Store::STATUS_FAILED === $this->store->get_status( $action_id )
&& $this->recurring_action_is_consistently_failing( $action, $action_id )
) {
ActionScheduler_Logger::instance()->log(
$action_id,
__( 'This action appears to be consistently failing. A new instance will not be scheduled.', 'woocommerce' )
);
return;
}
try {
ActionScheduler::factory()->repeat( $action );
} catch ( Exception $e ) {
do_action( 'action_scheduler_failed_to_schedule_next_instance', $action_id, $e, $action );
}
}
/**
* Determine if the specified recurring action has been consistently failing.
*
* @param ActionScheduler_Action $action The recurring action to be rescheduled.
* @param int $action_id The ID of the recurring action.
*
* @return bool
*/
private function recurring_action_is_consistently_failing( ActionScheduler_Action $action, $action_id ) {
/**
* Controls the failure threshold for recurring actions.
*
* Before rescheduling a recurring action, we look at its status. If it failed, we then check if all of the most
* recent actions (upto the threshold set by this filter) sharing the same hook have also failed: if they have,
* that is considered consistent failure and a new instance of the action will not be scheduled.
*
* @param int $failure_threshold Number of actions of the same hook to examine for failure. Defaults to 5.
*/
$consistent_failure_threshold = (int) apply_filters( 'action_scheduler_recurring_action_failure_threshold', 5 );
// This query should find the earliest *failing* action (for the hook we are interested in) within our threshold.
$query_args = array(
'hook' => $action->get_hook(),
'status' => ActionScheduler_Store::STATUS_FAILED,
'date' => date_create( 'now', timezone_open( 'UTC' ) )->format( 'Y-m-d H:i:s' ),
'date_compare' => '<',
'per_page' => 1,
'offset' => $consistent_failure_threshold - 1
);
$first_failing_action_id = $this->store->query_actions( $query_args );
// If we didn't retrieve an action ID, then there haven't been enough failures for us to worry about.
if ( empty( $first_failing_action_id ) ) {
return false;
}
// Now let's fetch the first action (having the same hook) of *any status* within the same window.
unset( $query_args['status'] );
$first_action_id_with_the_same_hook = $this->store->query_actions( $query_args );
/**
* If a recurring action is assessed as consistently failing, it will not be rescheduled. This hook provides a
* way to observe and optionally override that assessment.
*
* @param bool $is_consistently_failing If the action is considered to be consistently failing.
* @param ActionScheduler_Action $action The action being assessed.
*/
return (bool) apply_filters(
'action_scheduler_recurring_action_is_consistently_failing',
$first_action_id_with_the_same_hook === $first_failing_action_id,
$action
);
}
/**
* Run the queue cleaner.
*
* @author Jeremy Pry
*/
protected function run_cleanup() {
$this->cleaner->clean( 10 * $this->get_time_limit() );
}
/**
* Get the number of concurrent batches a runner allows.
*
* @return int
*/
public function get_allowed_concurrent_batches() {
return apply_filters( 'action_scheduler_queue_runner_concurrent_batches', 1 );
}
/**
* Check if the number of allowed concurrent batches is met or exceeded.
*
* @return bool
*/
public function has_maximum_concurrent_batches() {
return $this->store->get_claim_count() >= $this->get_allowed_concurrent_batches();
}
/**
* Get the maximum number of seconds a batch can run for.
*
* @return int The number of seconds.
*/
protected function get_time_limit() {
$time_limit = 30;
// Apply deprecated filter from deprecated get_maximum_execution_time() method
if ( has_filter( 'action_scheduler_maximum_execution_time' ) ) {
_deprecated_function( 'action_scheduler_maximum_execution_time', '2.1.1', 'action_scheduler_queue_runner_time_limit' );
$time_limit = apply_filters( 'action_scheduler_maximum_execution_time', $time_limit );
}
return absint( apply_filters( 'action_scheduler_queue_runner_time_limit', $time_limit ) );
}
/**
* Get the number of seconds the process has been running.
*
* @return int The number of seconds.
*/
protected function get_execution_time() {
$execution_time = microtime( true ) - $this->created_time;
// Get the CPU time if the hosting environment uses it rather than wall-clock time to calculate a process's execution time.
if ( function_exists( 'getrusage' ) && apply_filters( 'action_scheduler_use_cpu_execution_time', defined( 'PANTHEON_ENVIRONMENT' ) ) ) {
$resource_usages = getrusage();
if ( isset( $resource_usages['ru_stime.tv_usec'], $resource_usages['ru_stime.tv_usec'] ) ) {
$execution_time = $resource_usages['ru_stime.tv_sec'] + ( $resource_usages['ru_stime.tv_usec'] / 1000000 );
}
}
return $execution_time;
}
/**
* Check if the host's max execution time is (likely) to be exceeded if processing more actions.
*
* @param int $processed_actions The number of actions processed so far - used to determine the likelihood of exceeding the time limit if processing another action
* @return bool
*/
protected function time_likely_to_be_exceeded( $processed_actions ) {
$execution_time = $this->get_execution_time();
$max_execution_time = $this->get_time_limit();
// Safety against division by zero errors.
if ( 0 === $processed_actions ) {
return $execution_time >= $max_execution_time;
}
$time_per_action = $execution_time / $processed_actions;
$estimated_time = $execution_time + ( $time_per_action * 3 );
$likely_to_be_exceeded = $estimated_time > $max_execution_time;
return apply_filters( 'action_scheduler_maximum_execution_time_likely_to_be_exceeded', $likely_to_be_exceeded, $this, $processed_actions, $execution_time, $max_execution_time );
}
/**
* Get memory limit
*
* Based on WP_Background_Process::get_memory_limit()
*
* @return int
*/
protected function get_memory_limit() {
if ( function_exists( 'ini_get' ) ) {
$memory_limit = ini_get( 'memory_limit' );
} else {
$memory_limit = '128M'; // Sensible default, and minimum required by WooCommerce
}
if ( ! $memory_limit || -1 === $memory_limit || '-1' === $memory_limit ) {
// Unlimited, set to 32GB.
$memory_limit = '32G';
}
return ActionScheduler_Compatibility::convert_hr_to_bytes( $memory_limit );
}
/**
* Memory exceeded
*
* Ensures the batch process never exceeds 90% of the maximum WordPress memory.
*
* Based on WP_Background_Process::memory_exceeded()
*
* @return bool
*/
protected function memory_exceeded() {
$memory_limit = $this->get_memory_limit() * 0.90;
$current_memory = memory_get_usage( true );
$memory_exceeded = $current_memory >= $memory_limit;
return apply_filters( 'action_scheduler_memory_exceeded', $memory_exceeded, $this );
}
/**
* See if the batch limits have been exceeded, which is when memory usage is almost at
* the maximum limit, or the time to process more actions will exceed the max time limit.
*
* Based on WC_Background_Process::batch_limits_exceeded()
*
* @param int $processed_actions The number of actions processed so far - used to determine the likelihood of exceeding the time limit if processing another action
* @return bool
*/
protected function batch_limits_exceeded( $processed_actions ) {
return $this->memory_exceeded() || $this->time_likely_to_be_exceeded( $processed_actions );
}
/**
* Process actions in the queue.
*
* @author Jeremy Pry
* @param string $context Optional identifer for the context in which this action is being processed, e.g. 'WP CLI' or 'WP Cron'
* Generally, this should be capitalised and not localised as it's a proper noun.
* @return int The number of actions processed.
*/
abstract public function run( $context = '' );
}

View File

@@ -0,0 +1,103 @@
<?php
// phpcs:ignoreFile
/**
* Class ActionScheduler_Abstract_RecurringSchedule
*/
abstract class ActionScheduler_Abstract_RecurringSchedule extends ActionScheduler_Abstract_Schedule {
/**
* The date & time the first instance of this schedule was setup to run (which may not be this instance).
*
* Schedule objects are attached to an action object. Each schedule stores the run date for that
* object as the start date - @see $this->start - and logic to calculate the next run date after
* that - @see $this->calculate_next(). The $first_date property also keeps a record of when the very
* first instance of this chain of schedules ran.
*
* @var DateTime
*/
private $first_date = NULL;
/**
* Timestamp equivalent of @see $this->first_date
*
* @var int
*/
protected $first_timestamp = NULL;
/**
* The recurrance between each time an action is run using this schedule.
* Used to calculate the start date & time. Can be a number of seconds, in the
* case of ActionScheduler_IntervalSchedule, or a cron expression, as in the
* case of ActionScheduler_CronSchedule. Or something else.
*
* @var mixed
*/
protected $recurrence;
/**
* @param DateTime $date The date & time to run the action.
* @param mixed $recurrence The data used to determine the schedule's recurrance.
* @param DateTime|null $first (Optional) The date & time the first instance of this interval schedule ran. Default null, meaning this is the first instance.
*/
public function __construct( DateTime $date, $recurrence, DateTime $first = null ) {
parent::__construct( $date );
$this->first_date = empty( $first ) ? $date : $first;
$this->recurrence = $recurrence;
}
/**
* @return bool
*/
public function is_recurring() {
return true;
}
/**
* Get the date & time of the first schedule in this recurring series.
*
* @return DateTime|null
*/
public function get_first_date() {
return clone $this->first_date;
}
/**
* @return string
*/
public function get_recurrence() {
return $this->recurrence;
}
/**
* For PHP 5.2 compat, since DateTime objects can't be serialized
* @return array
*/
public function __sleep() {
$sleep_params = parent::__sleep();
$this->first_timestamp = $this->first_date->getTimestamp();
return array_merge( $sleep_params, array(
'first_timestamp',
'recurrence'
) );
}
/**
* Unserialize recurring schedules serialized/stored prior to AS 3.0.0
*
* Prior to Action Scheduler 3.0.0, schedules used different property names to refer
* to equivalent data. For example, ActionScheduler_IntervalSchedule::start_timestamp
* was the same as ActionScheduler_SimpleSchedule::timestamp. This was addressed in
* Action Scheduler 3.0.0, where properties and property names were aligned for better
* inheritance. To maintain backward compatibility with scheduled serialized and stored
* prior to 3.0, we need to correctly map the old property names.
*/
public function __wakeup() {
parent::__wakeup();
if ( $this->first_timestamp > 0 ) {
$this->first_date = as_get_datetime_object( $this->first_timestamp );
} else {
$this->first_date = $this->get_date();
}
}
}

View File

@@ -0,0 +1,84 @@
<?php
// phpcs:ignoreFile
/**
* Class ActionScheduler_Abstract_Schedule
*/
abstract class ActionScheduler_Abstract_Schedule extends ActionScheduler_Schedule_Deprecated {
/**
* The date & time the schedule is set to run.
*
* @var DateTime
*/
private $scheduled_date = NULL;
/**
* Timestamp equivalent of @see $this->scheduled_date
*
* @var int
*/
protected $scheduled_timestamp = NULL;
/**
* @param DateTime $date The date & time to run the action.
*/
public function __construct( DateTime $date ) {
$this->scheduled_date = $date;
}
/**
* Check if a schedule should recur.
*
* @return bool
*/
abstract public function is_recurring();
/**
* Calculate when the next instance of this schedule would run based on a given date & time.
*
* @param DateTime $after
* @return DateTime
*/
abstract protected function calculate_next( DateTime $after );
/**
* Get the next date & time when this schedule should run after a given date & time.
*
* @param DateTime $after
* @return DateTime|null
*/
public function get_next( DateTime $after ) {
$after = clone $after;
if ( $after > $this->scheduled_date ) {
$after = $this->calculate_next( $after );
return $after;
}
return clone $this->scheduled_date;
}
/**
* Get the date & time the schedule is set to run.
*
* @return DateTime|null
*/
public function get_date() {
return $this->scheduled_date;
}
/**
* For PHP 5.2 compat, since DateTime objects can't be serialized
* @return array
*/
public function __sleep() {
$this->scheduled_timestamp = $this->scheduled_date->getTimestamp();
return array(
'scheduled_timestamp',
);
}
public function __wakeup() {
$this->scheduled_date = as_get_datetime_object( $this->scheduled_timestamp );
unset( $this->scheduled_timestamp );
}
}

View File

@@ -0,0 +1,178 @@
<?php
// phpcs:ignoreFile
/**
* Class ActionScheduler_Abstract_Schema
*
* @package Action_Scheduler
*
* @codeCoverageIgnore
*
* Utility class for creating/updating custom tables
*/
abstract class ActionScheduler_Abstract_Schema {
/**
* @var int Increment this value in derived class to trigger a schema update.
*/
protected $schema_version = 1;
/**
* @var string Schema version stored in database.
*/
protected $db_version;
/**
* @var array Names of tables that will be registered by this class.
*/
protected $tables = array();
/**
* Can optionally be used by concrete classes to carry out additional initialization work
* as needed.
*/
public function init() {}
/**
* Register tables with WordPress, and create them if needed.
*
* @param bool $force_update Optional. Default false. Use true to always run the schema update.
*
* @return void
*/
public function register_tables( $force_update = false ) {
global $wpdb;
// make WP aware of our tables
foreach ( $this->tables as $table ) {
$wpdb->tables[] = $table;
$name = $this->get_full_table_name( $table );
$wpdb->$table = $name;
}
// create the tables
if ( $this->schema_update_required() || $force_update ) {
foreach ( $this->tables as $table ) {
/**
* Allow custom processing before updating a table schema.
*
* @param string $table Name of table being updated.
* @param string $db_version Existing version of the table being updated.
*/
do_action( 'action_scheduler_before_schema_update', $table, $this->db_version );
$this->update_table( $table );
}
$this->mark_schema_update_complete();
}
}
/**
* @param string $table The name of the table
*
* @return string The CREATE TABLE statement, suitable for passing to dbDelta
*/
abstract protected function get_table_definition( $table );
/**
* Determine if the database schema is out of date
* by comparing the integer found in $this->schema_version
* with the option set in the WordPress options table
*
* @return bool
*/
private function schema_update_required() {
$option_name = 'schema-' . static::class;
$this->db_version = get_option( $option_name, 0 );
// Check for schema option stored by the Action Scheduler Custom Tables plugin in case site has migrated from that plugin with an older schema
if ( 0 === $this->db_version ) {
$plugin_option_name = 'schema-';
switch ( static::class ) {
case 'ActionScheduler_StoreSchema':
$plugin_option_name .= 'Action_Scheduler\Custom_Tables\DB_Store_Table_Maker';
break;
case 'ActionScheduler_LoggerSchema':
$plugin_option_name .= 'Action_Scheduler\Custom_Tables\DB_Logger_Table_Maker';
break;
}
$this->db_version = get_option( $plugin_option_name, 0 );
delete_option( $plugin_option_name );
}
return version_compare( $this->db_version, $this->schema_version, '<' );
}
/**
* Update the option in WordPress to indicate that
* our schema is now up to date
*
* @return void
*/
private function mark_schema_update_complete() {
$option_name = 'schema-' . static::class;
// work around race conditions and ensure that our option updates
$value_to_save = (string) $this->schema_version . '.0.' . time();
update_option( $option_name, $value_to_save );
}
/**
* Update the schema for the given table
*
* @param string $table The name of the table to update
*
* @return void
*/
private function update_table( $table ) {
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
$definition = $this->get_table_definition( $table );
if ( $definition ) {
$updated = dbDelta( $definition );
foreach ( $updated as $updated_table => $update_description ) {
if ( strpos( $update_description, 'Created table' ) === 0 ) {
do_action( 'action_scheduler/created_table', $updated_table, $table );
}
}
}
}
/**
* @param string $table
*
* @return string The full name of the table, including the
* table prefix for the current blog
*/
protected function get_full_table_name( $table ) {
return $GLOBALS['wpdb']->prefix . $table;
}
/**
* Confirms that all of the tables registered by this schema class have been created.
*
* @return bool
*/
public function tables_exist() {
global $wpdb;
$tables_exist = true;
foreach ( $this->tables as $table_name ) {
$table_name = $wpdb->prefix . $table_name;
$pattern = str_replace( '_', '\\_', $table_name );
$existing_table = $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $pattern ) );
if ( $existing_table !== $table_name ) {
$tables_exist = false;
break;
}
}
return $tables_exist;
}
}

View File

@@ -0,0 +1,65 @@
<?php
// phpcs:ignoreFile
/**
* Abstract class for setting a basic lock to throttle some action.
*
* Class ActionScheduler_Lock
*/
abstract class ActionScheduler_Lock {
/** @var ActionScheduler_Lock */
private static $locker = NULL;
/** @var int */
protected static $lock_duration = MINUTE_IN_SECONDS;
/**
* Check if a lock is set for a given lock type.
*
* @param string $lock_type A string to identify different lock types.
* @return bool
*/
public function is_locked( $lock_type ) {
return ( $this->get_expiration( $lock_type ) >= time() );
}
/**
* Set a lock.
*
* To prevent race conditions, implementations should avoid setting the lock if the lock is already held.
*
* @param string $lock_type A string to identify different lock types.
* @return bool
*/
abstract public function set( $lock_type );
/**
* If a lock is set, return the timestamp it was set to expiry.
*
* @param string $lock_type A string to identify different lock types.
* @return bool|int False if no lock is set, otherwise the timestamp for when the lock is set to expire.
*/
abstract public function get_expiration( $lock_type );
/**
* Get the amount of time to set for a given lock. 60 seconds by default.
*
* @param string $lock_type A string to identify different lock types.
* @return int
*/
protected function get_duration( $lock_type ) {
return apply_filters( 'action_scheduler_lock_duration', self::$lock_duration, $lock_type );
}
/**
* @return ActionScheduler_Lock
*/
public static function instance() {
if ( empty( self::$locker ) ) {
$class = apply_filters( 'action_scheduler_lock_class', 'ActionScheduler_OptionLock' );
self::$locker = new $class();
}
return self::$locker;
}
}

View File

@@ -0,0 +1,177 @@
<?php
// phpcs:ignoreFile
/**
* Class ActionScheduler_Logger
* @codeCoverageIgnore
*/
abstract class ActionScheduler_Logger {
private static $logger = NULL;
/**
* @return ActionScheduler_Logger
*/
public static function instance() {
if ( empty(self::$logger) ) {
$class = apply_filters('action_scheduler_logger_class', 'ActionScheduler_wpCommentLogger');
self::$logger = new $class();
}
return self::$logger;
}
/**
* @param string $action_id
* @param string $message
* @param DateTime $date
*
* @return string The log entry ID
*/
abstract public function log( $action_id, $message, DateTime $date = NULL );
/**
* @param string $entry_id
*
* @return ActionScheduler_LogEntry
*/
abstract public function get_entry( $entry_id );
/**
* @param string $action_id
*
* @return ActionScheduler_LogEntry[]
*/
abstract public function get_logs( $action_id );
/**
* @codeCoverageIgnore
*/
public function init() {
$this->hook_stored_action();
add_action( 'action_scheduler_canceled_action', array( $this, 'log_canceled_action' ), 10, 1 );
add_action( 'action_scheduler_begin_execute', array( $this, 'log_started_action' ), 10, 2 );
add_action( 'action_scheduler_after_execute', array( $this, 'log_completed_action' ), 10, 3 );
add_action( 'action_scheduler_failed_execution', array( $this, 'log_failed_action' ), 10, 3 );
add_action( 'action_scheduler_failed_action', array( $this, 'log_timed_out_action' ), 10, 2 );
add_action( 'action_scheduler_unexpected_shutdown', array( $this, 'log_unexpected_shutdown' ), 10, 2 );
add_action( 'action_scheduler_reset_action', array( $this, 'log_reset_action' ), 10, 1 );
add_action( 'action_scheduler_execution_ignored', array( $this, 'log_ignored_action' ), 10, 2 );
add_action( 'action_scheduler_failed_fetch_action', array( $this, 'log_failed_fetch_action' ), 10, 2 );
add_action( 'action_scheduler_failed_to_schedule_next_instance', array( $this, 'log_failed_schedule_next_instance' ), 10, 2 );
add_action( 'action_scheduler_bulk_cancel_actions', array( $this, 'bulk_log_cancel_actions' ), 10, 1 );
}
public function hook_stored_action() {
add_action( 'action_scheduler_stored_action', array( $this, 'log_stored_action' ) );
}
public function unhook_stored_action() {
remove_action( 'action_scheduler_stored_action', array( $this, 'log_stored_action' ) );
}
public function log_stored_action( $action_id ) {
$this->log( $action_id, __( 'action created', 'woocommerce' ) );
}
public function log_canceled_action( $action_id ) {
$this->log( $action_id, __( 'action canceled', 'woocommerce' ) );
}
public function log_started_action( $action_id, $context = '' ) {
if ( ! empty( $context ) ) {
/* translators: %s: context */
$message = sprintf( __( 'action started via %s', 'woocommerce' ), $context );
} else {
$message = __( 'action started', 'woocommerce' );
}
$this->log( $action_id, $message );
}
public function log_completed_action( $action_id, $action = NULL, $context = '' ) {
if ( ! empty( $context ) ) {
/* translators: %s: context */
$message = sprintf( __( 'action complete via %s', 'woocommerce' ), $context );
} else {
$message = __( 'action complete', 'woocommerce' );
}
$this->log( $action_id, $message );
}
public function log_failed_action( $action_id, Exception $exception, $context = '' ) {
if ( ! empty( $context ) ) {
/* translators: 1: context 2: exception message */
$message = sprintf( __( 'action failed via %1$s: %2$s', 'woocommerce' ), $context, $exception->getMessage() );
} else {
/* translators: %s: exception message */
$message = sprintf( __( 'action failed: %s', 'woocommerce' ), $exception->getMessage() );
}
$this->log( $action_id, $message );
}
public function log_timed_out_action( $action_id, $timeout ) {
/* translators: %s: amount of time */
$this->log( $action_id, sprintf( __( 'action marked as failed after %s seconds. Unknown error occurred. Check server, PHP and database error logs to diagnose cause.', 'woocommerce' ), $timeout ) );
}
public function log_unexpected_shutdown( $action_id, $error ) {
if ( ! empty( $error ) ) {
/* translators: 1: error message 2: filename 3: line */
$this->log( $action_id, sprintf( __( 'unexpected shutdown: PHP Fatal error %1$s in %2$s on line %3$s', 'woocommerce' ), $error['message'], $error['file'], $error['line'] ) );
}
}
public function log_reset_action( $action_id ) {
$this->log( $action_id, __( 'action reset', 'woocommerce' ) );
}
public function log_ignored_action( $action_id, $context = '' ) {
if ( ! empty( $context ) ) {
/* translators: %s: context */
$message = sprintf( __( 'action ignored via %s', 'woocommerce' ), $context );
} else {
$message = __( 'action ignored', 'woocommerce' );
}
$this->log( $action_id, $message );
}
/**
* @param string $action_id
* @param Exception|NULL $exception The exception which occured when fetching the action. NULL by default for backward compatibility.
*
* @return ActionScheduler_LogEntry[]
*/
public function log_failed_fetch_action( $action_id, Exception $exception = NULL ) {
if ( ! is_null( $exception ) ) {
/* translators: %s: exception message */
$log_message = sprintf( __( 'There was a failure fetching this action: %s', 'woocommerce' ), $exception->getMessage() );
} else {
$log_message = __( 'There was a failure fetching this action', 'woocommerce' );
}
$this->log( $action_id, $log_message );
}
public function log_failed_schedule_next_instance( $action_id, Exception $exception ) {
/* translators: %s: exception message */
$this->log( $action_id, sprintf( __( 'There was a failure scheduling the next instance of this action: %s', 'woocommerce' ), $exception->getMessage() ) );
}
/**
* Bulk add cancel action log entries.
*
* Implemented here for backward compatibility. Should be implemented in parent loggers
* for more performant bulk logging.
*
* @param array $action_ids List of action ID.
*/
public function bulk_log_cancel_actions( $action_ids ) {
if ( empty( $action_ids ) ) {
return;
}
foreach ( $action_ids as $action_id ) {
$this->log_canceled_action( $action_id );
}
}
}

View File

@@ -0,0 +1,451 @@
<?php
// phpcs:ignoreFile
/**
* Class ActionScheduler_Store
* @codeCoverageIgnore
*/
abstract class ActionScheduler_Store extends ActionScheduler_Store_Deprecated {
const STATUS_COMPLETE = 'complete';
const STATUS_PENDING = 'pending';
const STATUS_RUNNING = 'in-progress';
const STATUS_FAILED = 'failed';
const STATUS_CANCELED = 'canceled';
const DEFAULT_CLASS = 'ActionScheduler_wpPostStore';
/** @var ActionScheduler_Store */
private static $store = NULL;
/** @var int */
protected static $max_args_length = 191;
/**
* @param ActionScheduler_Action $action
* @param DateTime $scheduled_date Optional Date of the first instance
* to store. Otherwise uses the first date of the action's
* schedule.
*
* @return int The action ID
*/
abstract public function save_action( ActionScheduler_Action $action, DateTime $scheduled_date = NULL );
/**
* @param string $action_id
*
* @return ActionScheduler_Action
*/
abstract public function fetch_action( $action_id );
/**
* Find an action.
*
* Note: the query ordering changes based on the passed 'status' value.
*
* @param string $hook Action hook.
* @param array $params Parameters of the action to find.
*
* @return string|null ID of the next action matching the criteria or NULL if not found.
*/
public function find_action( $hook, $params = array() ) {
$params = wp_parse_args(
$params,
array(
'args' => null,
'status' => self::STATUS_PENDING,
'group' => '',
)
);
// These params are fixed for this method.
$params['hook'] = $hook;
$params['orderby'] = 'date';
$params['per_page'] = 1;
if ( ! empty( $params['status'] ) ) {
if ( self::STATUS_PENDING === $params['status'] ) {
$params['order'] = 'ASC'; // Find the next action that matches.
} else {
$params['order'] = 'DESC'; // Find the most recent action that matches.
}
}
$results = $this->query_actions( $params );
return empty( $results ) ? null : $results[0];
}
/**
* Query for action count or list of action IDs.
*
* @since 3.3.0 $query['status'] accepts array of statuses instead of a single status.
*
* @param array $query {
* Query filtering options.
*
* @type string $hook The name of the actions. Optional.
* @type string|array $status The status or statuses of the actions. Optional.
* @type array $args The args array of the actions. Optional.
* @type DateTime $date The scheduled date of the action. Used in UTC timezone. Optional.
* @type string $date_compare Operator for selecting by $date param. Accepted values are '!=', '>', '>=', '<', '<=', '='. Defaults to '<='.
* @type DateTime $modified The last modified date of the action. Used in UTC timezone. Optional.
* @type string $modified_compare Operator for comparing $modified param. Accepted values are '!=', '>', '>=', '<', '<=', '='. Defaults to '<='.
* @type string $group The group the action belongs to. Optional.
* @type bool|int $claimed TRUE to find claimed actions, FALSE to find unclaimed actions, an int to find a specific claim ID. Optional.
* @type int $per_page Number of results to return. Defaults to 5.
* @type int $offset The query pagination offset. Defaults to 0.
* @type int $orderby Accepted values are 'hook', 'group', 'modified', 'date' or 'none'. Defaults to 'date'.
* @type string $order Accepted values are 'ASC' or 'DESC'. Defaults to 'ASC'.
* }
* @param string $query_type Whether to select or count the results. Default, select.
*
* @return string|array|null The IDs of actions matching the query. Null on failure.
*/
abstract public function query_actions( $query = array(), $query_type = 'select' );
/**
* Run query to get a single action ID.
*
* @since 3.3.0
*
* @see ActionScheduler_Store::query_actions for $query arg usage but 'per_page' and 'offset' can't be used.
*
* @param array $query Query parameters.
*
* @return int|null
*/
public function query_action( $query ) {
$query['per_page'] = 1;
$query['offset'] = 0;
$results = $this->query_actions( $query );
if ( empty( $results ) ) {
return null;
} else {
return (int) $results[0];
}
}
/**
* Get a count of all actions in the store, grouped by status
*
* @return array
*/
abstract public function action_counts();
/**
* Get additional action counts.
*
* - add past-due actions
*
* @return array
*/
public function extra_action_counts() {
$extra_actions = array();
$pastdue_action_counts = ( int ) $this->query_actions( array(
'status' => self::STATUS_PENDING,
'date' => as_get_datetime_object(),
), 'count' );
if ( $pastdue_action_counts ) {
$extra_actions['past-due'] = $pastdue_action_counts;
}
/**
* Allows 3rd party code to add extra action counts (used in filters in the list table).
*
* @since 3.5.0
* @param $extra_actions array Array with format action_count_identifier => action count.
*/
return apply_filters( 'action_scheduler_extra_action_counts', $extra_actions );
}
/**
* @param string $action_id
*/
abstract public function cancel_action( $action_id );
/**
* @param string $action_id
*/
abstract public function delete_action( $action_id );
/**
* @param string $action_id
*
* @return DateTime The date the action is schedule to run, or the date that it ran.
*/
abstract public function get_date( $action_id );
/**
* @param int $max_actions
* @param DateTime $before_date Claim only actions schedule before the given date. Defaults to now.
* @param array $hooks Claim only actions with a hook or hooks.
* @param string $group Claim only actions in the given group.
*
* @return ActionScheduler_ActionClaim
*/
abstract public function stake_claim( $max_actions = 10, DateTime $before_date = null, $hooks = array(), $group = '' );
/**
* @return int
*/
abstract public function get_claim_count();
/**
* @param ActionScheduler_ActionClaim $claim
*/
abstract public function release_claim( ActionScheduler_ActionClaim $claim );
/**
* @param string $action_id
*/
abstract public function unclaim_action( $action_id );
/**
* @param string $action_id
*/
abstract public function mark_failure( $action_id );
/**
* @param string $action_id
*/
abstract public function log_execution( $action_id );
/**
* @param string $action_id
*/
abstract public function mark_complete( $action_id );
/**
* @param string $action_id
*
* @return string
*/
abstract public function get_status( $action_id );
/**
* @param string $action_id
* @return mixed
*/
abstract public function get_claim_id( $action_id );
/**
* @param string $claim_id
* @return array
*/
abstract public function find_actions_by_claim_id( $claim_id );
/**
* @param string $comparison_operator
* @return string
*/
protected function validate_sql_comparator( $comparison_operator ) {
if ( in_array( $comparison_operator, array('!=', '>', '>=', '<', '<=', '=') ) ) {
return $comparison_operator;
}
return '=';
}
/**
* Get the time MySQL formated date/time string for an action's (next) scheduled date.
*
* @param ActionScheduler_Action $action
* @param DateTime $scheduled_date (optional)
* @return string
*/
protected function get_scheduled_date_string( ActionScheduler_Action $action, DateTime $scheduled_date = NULL ) {
$next = null === $scheduled_date ? $action->get_schedule()->get_date() : $scheduled_date;
if ( ! $next ) {
$next = date_create();
}
$next->setTimezone( new DateTimeZone( 'UTC' ) );
return $next->format( 'Y-m-d H:i:s' );
}
/**
* Get the time MySQL formated date/time string for an action's (next) scheduled date.
*
* @param ActionScheduler_Action $action
* @param DateTime $scheduled_date (optional)
* @return string
*/
protected function get_scheduled_date_string_local( ActionScheduler_Action $action, DateTime $scheduled_date = NULL ) {
$next = null === $scheduled_date ? $action->get_schedule()->get_date() : $scheduled_date;
if ( ! $next ) {
$next = date_create();
}
ActionScheduler_TimezoneHelper::set_local_timezone( $next );
return $next->format( 'Y-m-d H:i:s' );
}
/**
* Validate that we could decode action arguments.
*
* @param mixed $args The decoded arguments.
* @param int $action_id The action ID.
*
* @throws ActionScheduler_InvalidActionException When the decoded arguments are invalid.
*/
protected function validate_args( $args, $action_id ) {
// Ensure we have an array of args.
if ( ! is_array( $args ) ) {
throw ActionScheduler_InvalidActionException::from_decoding_args( $action_id );
}
// Validate JSON decoding if possible.
if ( function_exists( 'json_last_error' ) && JSON_ERROR_NONE !== json_last_error() ) {
throw ActionScheduler_InvalidActionException::from_decoding_args( $action_id, $args );
}
}
/**
* Validate a ActionScheduler_Schedule object.
*
* @param mixed $schedule The unserialized ActionScheduler_Schedule object.
* @param int $action_id The action ID.
*
* @throws ActionScheduler_InvalidActionException When the schedule is invalid.
*/
protected function validate_schedule( $schedule, $action_id ) {
if ( empty( $schedule ) || ! is_a( $schedule, 'ActionScheduler_Schedule' ) ) {
throw ActionScheduler_InvalidActionException::from_schedule( $action_id, $schedule );
}
}
/**
* InnoDB indexes have a maximum size of 767 bytes by default, which is only 191 characters with utf8mb4.
*
* Previously, AS wasn't concerned about args length, as we used the (unindex) post_content column. However,
* with custom tables, we use an indexed VARCHAR column instead.
*
* @param ActionScheduler_Action $action Action to be validated.
* @throws InvalidArgumentException When json encoded args is too long.
*/
protected function validate_action( ActionScheduler_Action $action ) {
if ( strlen( json_encode( $action->get_args() ) ) > static::$max_args_length ) {
throw new InvalidArgumentException( sprintf( __( 'ActionScheduler_Action::$args too long. To ensure the args column can be indexed, action args should not be more than %d characters when encoded as JSON.', 'woocommerce' ), static::$max_args_length ) );
}
}
/**
* Cancel pending actions by hook.
*
* @since 3.0.0
*
* @param string $hook Hook name.
*
* @return void
*/
public function cancel_actions_by_hook( $hook ) {
$action_ids = true;
while ( ! empty( $action_ids ) ) {
$action_ids = $this->query_actions(
array(
'hook' => $hook,
'status' => self::STATUS_PENDING,
'per_page' => 1000,
'orderby' => 'none',
)
);
$this->bulk_cancel_actions( $action_ids );
}
}
/**
* Cancel pending actions by group.
*
* @since 3.0.0
*
* @param string $group Group slug.
*
* @return void
*/
public function cancel_actions_by_group( $group ) {
$action_ids = true;
while ( ! empty( $action_ids ) ) {
$action_ids = $this->query_actions(
array(
'group' => $group,
'status' => self::STATUS_PENDING,
'per_page' => 1000,
'orderby' => 'none',
)
);
$this->bulk_cancel_actions( $action_ids );
}
}
/**
* Cancel a set of action IDs.
*
* @since 3.0.0
*
* @param array $action_ids List of action IDs.
*
* @return void
*/
private function bulk_cancel_actions( $action_ids ) {
foreach ( $action_ids as $action_id ) {
$this->cancel_action( $action_id );
}
do_action( 'action_scheduler_bulk_cancel_actions', $action_ids );
}
/**
* @return array
*/
public function get_status_labels() {
return array(
self::STATUS_COMPLETE => __( 'Complete', 'woocommerce' ),
self::STATUS_PENDING => __( 'Pending', 'woocommerce' ),
self::STATUS_RUNNING => __( 'In-progress', 'woocommerce' ),
self::STATUS_FAILED => __( 'Failed', 'woocommerce' ),
self::STATUS_CANCELED => __( 'Canceled', 'woocommerce' ),
);
}
/**
* Check if there are any pending scheduled actions due to run.
*
* @param ActionScheduler_Action $action
* @param DateTime $scheduled_date (optional)
* @return string
*/
public function has_pending_actions_due() {
$pending_actions = $this->query_actions( array(
'date' => as_get_datetime_object(),
'status' => ActionScheduler_Store::STATUS_PENDING,
'orderby' => 'none',
) );
return ! empty( $pending_actions );
}
/**
* Callable initialization function optionally overridden in derived classes.
*/
public function init() {}
/**
* Callable function to mark an action as migrated optionally overridden in derived classes.
*/
public function mark_migrated( $action_id ) {}
/**
* @return ActionScheduler_Store
*/
public static function instance() {
if ( empty( self::$store ) ) {
$class = apply_filters( 'action_scheduler_store_class', self::DEFAULT_CLASS );
self::$store = new $class();
}
return self::$store;
}
}

View File

@@ -0,0 +1,153 @@
<?php
// phpcs:ignoreFile
/**
* Class ActionScheduler_TimezoneHelper
*/
abstract class ActionScheduler_TimezoneHelper {
private static $local_timezone = NULL;
/**
* Set a DateTime's timezone to the WordPress site's timezone, or a UTC offset
* if no timezone string is available.
*
* @since 2.1.0
*
* @param DateTime $date
* @return ActionScheduler_DateTime
*/
public static function set_local_timezone( DateTime $date ) {
// Accept a DateTime for easier backward compatibility, even though we require methods on ActionScheduler_DateTime
if ( ! is_a( $date, 'ActionScheduler_DateTime' ) ) {
$date = as_get_datetime_object( $date->format( 'U' ) );
}
if ( get_option( 'timezone_string' ) ) {
$date->setTimezone( new DateTimeZone( self::get_local_timezone_string() ) );
} else {
$date->setUtcOffset( self::get_local_timezone_offset() );
}
return $date;
}
/**
* Helper to retrieve the timezone string for a site until a WP core method exists
* (see https://core.trac.wordpress.org/ticket/24730).
*
* Adapted from wc_timezone_string() and https://secure.php.net/manual/en/function.timezone-name-from-abbr.php#89155.
*
* If no timezone string is set, and its not possible to match the UTC offset set for the site to a timezone
* string, then an empty string will be returned, and the UTC offset should be used to set a DateTime's
* timezone.
*
* @since 2.1.0
* @return string PHP timezone string for the site or empty if no timezone string is available.
*/
protected static function get_local_timezone_string( $reset = false ) {
// If site timezone string exists, return it.
$timezone = get_option( 'timezone_string' );
if ( $timezone ) {
return $timezone;
}
// Get UTC offset, if it isn't set then return UTC.
$utc_offset = intval( get_option( 'gmt_offset', 0 ) );
if ( 0 === $utc_offset ) {
return 'UTC';
}
// Adjust UTC offset from hours to seconds.
$utc_offset *= 3600;
// Attempt to guess the timezone string from the UTC offset.
$timezone = timezone_name_from_abbr( '', $utc_offset );
if ( $timezone ) {
return $timezone;
}
// Last try, guess timezone string manually.
foreach ( timezone_abbreviations_list() as $abbr ) {
foreach ( $abbr as $city ) {
if ( (bool) date( 'I' ) === (bool) $city['dst'] && $city['timezone_id'] && intval( $city['offset'] ) === $utc_offset ) {
return $city['timezone_id'];
}
}
}
// No timezone string
return '';
}
/**
* Get timezone offset in seconds.
*
* @since 2.1.0
* @return float
*/
protected static function get_local_timezone_offset() {
$timezone = get_option( 'timezone_string' );
if ( $timezone ) {
$timezone_object = new DateTimeZone( $timezone );
return $timezone_object->getOffset( new DateTime( 'now' ) );
} else {
return floatval( get_option( 'gmt_offset', 0 ) ) * HOUR_IN_SECONDS;
}
}
/**
* @deprecated 2.1.0
*/
public static function get_local_timezone( $reset = FALSE ) {
_deprecated_function( __FUNCTION__, '2.1.0', 'ActionScheduler_TimezoneHelper::set_local_timezone()' );
if ( $reset ) {
self::$local_timezone = NULL;
}
if ( !isset(self::$local_timezone) ) {
$tzstring = get_option('timezone_string');
if ( empty($tzstring) ) {
$gmt_offset = get_option('gmt_offset');
if ( $gmt_offset == 0 ) {
$tzstring = 'UTC';
} else {
$gmt_offset *= HOUR_IN_SECONDS;
$tzstring = timezone_name_from_abbr( '', $gmt_offset, 1 );
// If there's no timezone string, try again with no DST.
if ( false === $tzstring ) {
$tzstring = timezone_name_from_abbr( '', $gmt_offset, 0 );
}
// Try mapping to the first abbreviation we can find.
if ( false === $tzstring ) {
$is_dst = date( 'I' );
foreach ( timezone_abbreviations_list() as $abbr ) {
foreach ( $abbr as $city ) {
if ( $city['dst'] == $is_dst && $city['offset'] == $gmt_offset ) {
// If there's no valid timezone ID, keep looking.
if ( null === $city['timezone_id'] ) {
continue;
}
$tzstring = $city['timezone_id'];
break 2;
}
}
}
}
// If we still have no valid string, then fall back to UTC.
if ( false === $tzstring ) {
$tzstring = 'UTC';
}
}
}
self::$local_timezone = new DateTimeZone($tzstring);
}
return self::$local_timezone;
}
}

View File

@@ -0,0 +1,136 @@
<?php
// phpcs:ignoreFile
/**
* Class ActionScheduler_Action
*/
class ActionScheduler_Action {
protected $hook = '';
protected $args = array();
/** @var ActionScheduler_Schedule */
protected $schedule = NULL;
protected $group = '';
/**
* Priorities are conceptually similar to those used for regular WordPress actions.
* Like those, a lower priority takes precedence over a higher priority and the default
* is 10.
*
* Unlike regular WordPress actions, the priority of a scheduled action is strictly an
* integer and should be kept within the bounds 0-255 (anything outside the bounds will
* be brought back into the acceptable range).
*
* @var int
*/
protected $priority = 10;
public function __construct( $hook, array $args = array(), ActionScheduler_Schedule $schedule = NULL, $group = '' ) {
$schedule = empty( $schedule ) ? new ActionScheduler_NullSchedule() : $schedule;
$this->set_hook($hook);
$this->set_schedule($schedule);
$this->set_args($args);
$this->set_group($group);
}
/**
* Executes the action.
*
* If no callbacks are registered, an exception will be thrown and the action will not be
* fired. This is useful to help detect cases where the code responsible for setting up
* a scheduled action no longer exists.
*
* @throws Exception If no callbacks are registered for this action.
*/
public function execute() {
$hook = $this->get_hook();
if ( ! has_action( $hook ) ) {
throw new Exception(
sprintf(
/* translators: 1: action hook. */
__( 'Scheduled action for %1$s will not be executed as no callbacks are registered.', 'woocommerce' ),
$hook
)
);
}
do_action_ref_array( $hook, array_values( $this->get_args() ) );
}
/**
* @param string $hook
*/
protected function set_hook( $hook ) {
$this->hook = $hook;
}
public function get_hook() {
return $this->hook;
}
protected function set_schedule( ActionScheduler_Schedule $schedule ) {
$this->schedule = $schedule;
}
/**
* @return ActionScheduler_Schedule
*/
public function get_schedule() {
return $this->schedule;
}
protected function set_args( array $args ) {
$this->args = $args;
}
public function get_args() {
return $this->args;
}
/**
* @param string $group
*/
protected function set_group( $group ) {
$this->group = $group;
}
/**
* @return string
*/
public function get_group() {
return $this->group;
}
/**
* @return bool If the action has been finished
*/
public function is_finished() {
return FALSE;
}
/**
* Sets the priority of the action.
*
* @param int $priority Priority level (lower is higher priority). Should be in the range 0-255.
*
* @return void
*/
public function set_priority( $priority ) {
if ( $priority < 0 ) {
$priority = 0;
} elseif ( $priority > 255 ) {
$priority = 255;
}
$this->priority = (int) $priority;
}
/**
* Gets the action priority.
*
* @return int
*/
public function get_priority() {
return $this->priority;
}
}

View File

@@ -0,0 +1,24 @@
<?php
// phpcs:ignoreFile
/**
* Class ActionScheduler_CanceledAction
*
* Stored action which was canceled and therefore acts like a finished action but should always return a null schedule,
* regardless of schedule passed to its constructor.
*/
class ActionScheduler_CanceledAction extends ActionScheduler_FinishedAction {
/**
* @param string $hook
* @param array $args
* @param ActionScheduler_Schedule $schedule
* @param string $group
*/
public function __construct( $hook, array $args = array(), ActionScheduler_Schedule $schedule = null, $group = '' ) {
parent::__construct( $hook, $args, $schedule, $group );
if ( is_null( $schedule ) ) {
$this->set_schedule( new ActionScheduler_NullSchedule() );
}
}
}

View File

@@ -0,0 +1,17 @@
<?php
// phpcs:ignoreFile
/**
* Class ActionScheduler_FinishedAction
*/
class ActionScheduler_FinishedAction extends ActionScheduler_Action {
public function execute() {
// don't execute
}
public function is_finished() {
return TRUE;
}
}

View File

@@ -0,0 +1,17 @@
<?php
// phpcs:ignoreFile
/**
* Class ActionScheduler_NullAction
*/
class ActionScheduler_NullAction extends ActionScheduler_Action {
public function __construct( $hook = '', array $args = array(), ActionScheduler_Schedule $schedule = NULL ) {
$this->set_schedule( new ActionScheduler_NullSchedule() );
}
public function execute() {
// don't execute
}
}

View File

@@ -0,0 +1,155 @@
<?php
// phpcs:ignoreFile
/**
* Class ActionScheduler_DBLogger
*
* Action logs data table data store.
*
* @since 3.0.0
*/
class ActionScheduler_DBLogger extends ActionScheduler_Logger {
/**
* Add a record to an action log.
*
* @param int $action_id Action ID.
* @param string $message Message to be saved in the log entry.
* @param DateTime $date Timestamp of the log entry.
*
* @return int The log entry ID.
*/
public function log( $action_id, $message, DateTime $date = null ) {
if ( empty( $date ) ) {
$date = as_get_datetime_object();
} else {
$date = clone $date;
}
$date_gmt = $date->format( 'Y-m-d H:i:s' );
ActionScheduler_TimezoneHelper::set_local_timezone( $date );
$date_local = $date->format( 'Y-m-d H:i:s' );
/** @var \wpdb $wpdb */ //phpcs:ignore Generic.Commenting.DocComment.MissingShort
global $wpdb;
$wpdb->insert(
$wpdb->actionscheduler_logs,
array(
'action_id' => $action_id,
'message' => $message,
'log_date_gmt' => $date_gmt,
'log_date_local' => $date_local,
),
array( '%d', '%s', '%s', '%s' )
);
return $wpdb->insert_id;
}
/**
* Retrieve an action log entry.
*
* @param int $entry_id Log entry ID.
*
* @return ActionScheduler_LogEntry
*/
public function get_entry( $entry_id ) {
/** @var \wpdb $wpdb */ //phpcs:ignore Generic.Commenting.DocComment.MissingShort
global $wpdb;
$entry = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->actionscheduler_logs} WHERE log_id=%d", $entry_id ) );
return $this->create_entry_from_db_record( $entry );
}
/**
* Create an action log entry from a database record.
*
* @param object $record Log entry database record object.
*
* @return ActionScheduler_LogEntry
*/
private function create_entry_from_db_record( $record ) {
if ( empty( $record ) ) {
return new ActionScheduler_NullLogEntry();
}
if ( is_null( $record->log_date_gmt ) ) {
$date = as_get_datetime_object( ActionScheduler_StoreSchema::DEFAULT_DATE );
} else {
$date = as_get_datetime_object( $record->log_date_gmt );
}
return new ActionScheduler_LogEntry( $record->action_id, $record->message, $date );
}
/**
* Retrieve an action's log entries from the database.
*
* @param int $action_id Action ID.
*
* @return ActionScheduler_LogEntry[]
*/
public function get_logs( $action_id ) {
/** @var \wpdb $wpdb */ //phpcs:ignore Generic.Commenting.DocComment.MissingShort
global $wpdb;
$records = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM {$wpdb->actionscheduler_logs} WHERE action_id=%d", $action_id ) );
return array_map( array( $this, 'create_entry_from_db_record' ), $records );
}
/**
* Initialize the data store.
*
* @codeCoverageIgnore
*/
public function init() {
$table_maker = new ActionScheduler_LoggerSchema();
$table_maker->init();
$table_maker->register_tables();
parent::init();
add_action( 'action_scheduler_deleted_action', array( $this, 'clear_deleted_action_logs' ), 10, 1 );
}
/**
* Delete the action logs for an action.
*
* @param int $action_id Action ID.
*/
public function clear_deleted_action_logs( $action_id ) {
/** @var \wpdb $wpdb */ //phpcs:ignore Generic.Commenting.DocComment.MissingShort
global $wpdb;
$wpdb->delete( $wpdb->actionscheduler_logs, array( 'action_id' => $action_id ), array( '%d' ) );
}
/**
* Bulk add cancel action log entries.
*
* @param array $action_ids List of action ID.
*/
public function bulk_log_cancel_actions( $action_ids ) {
if ( empty( $action_ids ) ) {
return;
}
/** @var \wpdb $wpdb */ //phpcs:ignore Generic.Commenting.DocComment.MissingShort
global $wpdb;
$date = as_get_datetime_object();
$date_gmt = $date->format( 'Y-m-d H:i:s' );
ActionScheduler_TimezoneHelper::set_local_timezone( $date );
$date_local = $date->format( 'Y-m-d H:i:s' );
$message = __( 'action canceled', 'woocommerce' );
$format = '(%d, ' . $wpdb->prepare( '%s, %s, %s', $message, $date_gmt, $date_local ) . ')';
$sql_query = "INSERT {$wpdb->actionscheduler_logs} (action_id, message, log_date_gmt, log_date_local) VALUES ";
$value_rows = array();
foreach ( $action_ids as $action_id ) {
$value_rows[] = $wpdb->prepare( $format, $action_id ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
}
$sql_query .= implode( ',', $value_rows );
$wpdb->query( $sql_query ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
}
}

View File

@@ -0,0 +1,427 @@
<?php
// phpcs:ignoreFile
use ActionScheduler_Store as Store;
use Action_Scheduler\Migration\Runner;
use Action_Scheduler\Migration\Config;
use Action_Scheduler\Migration\Controller;
/**
* Class ActionScheduler_HybridStore
*
* A wrapper around multiple stores that fetches data from both.
*
* @since 3.0.0
*/
class ActionScheduler_HybridStore extends Store {
const DEMARKATION_OPTION = 'action_scheduler_hybrid_store_demarkation';
private $primary_store;
private $secondary_store;
private $migration_runner;
/**
* @var int The dividing line between IDs of actions created
* by the primary and secondary stores.
*
* Methods that accept an action ID will compare the ID against
* this to determine which store will contain that ID. In almost
* all cases, the ID should come from the primary store, but if
* client code is bypassing the API functions and fetching IDs
* from elsewhere, then there is a chance that an unmigrated ID
* might be requested.
*/
private $demarkation_id = 0;
/**
* ActionScheduler_HybridStore constructor.
*
* @param Config $config Migration config object.
*/
public function __construct( Config $config = null ) {
$this->demarkation_id = (int) get_option( self::DEMARKATION_OPTION, 0 );
if ( empty( $config ) ) {
$config = Controller::instance()->get_migration_config_object();
}
$this->primary_store = $config->get_destination_store();
$this->secondary_store = $config->get_source_store();
$this->migration_runner = new Runner( $config );
}
/**
* Initialize the table data store tables.
*
* @codeCoverageIgnore
*/
public function init() {
add_action( 'action_scheduler/created_table', [ $this, 'set_autoincrement' ], 10, 2 );
$this->primary_store->init();
$this->secondary_store->init();
remove_action( 'action_scheduler/created_table', [ $this, 'set_autoincrement' ], 10 );
}
/**
* When the actions table is created, set its autoincrement
* value to be one higher than the posts table to ensure that
* there are no ID collisions.
*
* @param string $table_name
* @param string $table_suffix
*
* @return void
* @codeCoverageIgnore
*/
public function set_autoincrement( $table_name, $table_suffix ) {
if ( ActionScheduler_StoreSchema::ACTIONS_TABLE === $table_suffix ) {
if ( empty( $this->demarkation_id ) ) {
$this->demarkation_id = $this->set_demarkation_id();
}
/** @var \wpdb $wpdb */
global $wpdb;
/**
* A default date of '0000-00-00 00:00:00' is invalid in MySQL 5.7 when configured with
* sql_mode including both STRICT_TRANS_TABLES and NO_ZERO_DATE.
*/
$default_date = new DateTime( 'tomorrow' );
$null_action = new ActionScheduler_NullAction();
$date_gmt = $this->get_scheduled_date_string( $null_action, $default_date );
$date_local = $this->get_scheduled_date_string_local( $null_action, $default_date );
$row_count = $wpdb->insert(
$wpdb->{ActionScheduler_StoreSchema::ACTIONS_TABLE},
[
'action_id' => $this->demarkation_id,
'hook' => '',
'status' => '',
'scheduled_date_gmt' => $date_gmt,
'scheduled_date_local' => $date_local,
'last_attempt_gmt' => $date_gmt,
'last_attempt_local' => $date_local,
]
);
if ( $row_count > 0 ) {
$wpdb->delete(
$wpdb->{ActionScheduler_StoreSchema::ACTIONS_TABLE},
[ 'action_id' => $this->demarkation_id ]
);
}
}
}
/**
* Store the demarkation id in WP options.
*
* @param int $id The ID to set as the demarkation point between the two stores
* Leave null to use the next ID from the WP posts table.
*
* @return int The new ID.
*
* @codeCoverageIgnore
*/
private function set_demarkation_id( $id = null ) {
if ( empty( $id ) ) {
/** @var \wpdb $wpdb */
global $wpdb;
$id = (int) $wpdb->get_var( "SELECT MAX(ID) FROM $wpdb->posts" );
$id ++;
}
update_option( self::DEMARKATION_OPTION, $id );
return $id;
}
/**
* Find the first matching action from the secondary store.
* If it exists, migrate it to the primary store immediately.
* After it migrates, the secondary store will logically contain
* the next matching action, so return the result thence.
*
* @param string $hook
* @param array $params
*
* @return string
*/
public function find_action( $hook, $params = [] ) {
$found_unmigrated_action = $this->secondary_store->find_action( $hook, $params );
if ( ! empty( $found_unmigrated_action ) ) {
$this->migrate( [ $found_unmigrated_action ] );
}
return $this->primary_store->find_action( $hook, $params );
}
/**
* Find actions matching the query in the secondary source first.
* If any are found, migrate them immediately. Then the secondary
* store will contain the canonical results.
*
* @param array $query
* @param string $query_type Whether to select or count the results. Default, select.
*
* @return int[]
*/
public function query_actions( $query = [], $query_type = 'select' ) {
$found_unmigrated_actions = $this->secondary_store->query_actions( $query, 'select' );
if ( ! empty( $found_unmigrated_actions ) ) {
$this->migrate( $found_unmigrated_actions );
}
return $this->primary_store->query_actions( $query, $query_type );
}
/**
* Get a count of all actions in the store, grouped by status
*
* @return array Set of 'status' => int $count pairs for statuses with 1 or more actions of that status.
*/
public function action_counts() {
$unmigrated_actions_count = $this->secondary_store->action_counts();
$migrated_actions_count = $this->primary_store->action_counts();
$actions_count_by_status = array();
foreach ( $this->get_status_labels() as $status_key => $status_label ) {
$count = 0;
if ( isset( $unmigrated_actions_count[ $status_key ] ) ) {
$count += $unmigrated_actions_count[ $status_key ];
}
if ( isset( $migrated_actions_count[ $status_key ] ) ) {
$count += $migrated_actions_count[ $status_key ];
}
$actions_count_by_status[ $status_key ] = $count;
}
$actions_count_by_status = array_filter( $actions_count_by_status );
return $actions_count_by_status;
}
/**
* If any actions would have been claimed by the secondary store,
* migrate them immediately, then ask the primary store for the
* canonical claim.
*
* @param int $max_actions
* @param DateTime|null $before_date
*
* @return ActionScheduler_ActionClaim
*/
public function stake_claim( $max_actions = 10, DateTime $before_date = null, $hooks = array(), $group = '' ) {
$claim = $this->secondary_store->stake_claim( $max_actions, $before_date, $hooks, $group );
$claimed_actions = $claim->get_actions();
if ( ! empty( $claimed_actions ) ) {
$this->migrate( $claimed_actions );
}
$this->secondary_store->release_claim( $claim );
return $this->primary_store->stake_claim( $max_actions, $before_date, $hooks, $group );
}
/**
* Migrate a list of actions to the table data store.
*
* @param array $action_ids List of action IDs.
*/
private function migrate( $action_ids ) {
$this->migration_runner->migrate_actions( $action_ids );
}
/**
* Save an action to the primary store.
*
* @param ActionScheduler_Action $action Action object to be saved.
* @param DateTime $date Optional. Schedule date. Default null.
*
* @return int The action ID
*/
public function save_action( ActionScheduler_Action $action, DateTime $date = null ) {
return $this->primary_store->save_action( $action, $date );
}
/**
* Retrieve an existing action whether migrated or not.
*
* @param int $action_id Action ID.
*/
public function fetch_action( $action_id ) {
$store = $this->get_store_from_action_id( $action_id, true );
if ( $store ) {
return $store->fetch_action( $action_id );
} else {
return new ActionScheduler_NullAction();
}
}
/**
* Cancel an existing action whether migrated or not.
*
* @param int $action_id Action ID.
*/
public function cancel_action( $action_id ) {
$store = $this->get_store_from_action_id( $action_id );
if ( $store ) {
$store->cancel_action( $action_id );
}
}
/**
* Delete an existing action whether migrated or not.
*
* @param int $action_id Action ID.
*/
public function delete_action( $action_id ) {
$store = $this->get_store_from_action_id( $action_id );
if ( $store ) {
$store->delete_action( $action_id );
}
}
/**
* Get the schedule date an existing action whether migrated or not.
*
* @param int $action_id Action ID.
*/
public function get_date( $action_id ) {
$store = $this->get_store_from_action_id( $action_id );
if ( $store ) {
return $store->get_date( $action_id );
} else {
return null;
}
}
/**
* Mark an existing action as failed whether migrated or not.
*
* @param int $action_id Action ID.
*/
public function mark_failure( $action_id ) {
$store = $this->get_store_from_action_id( $action_id );
if ( $store ) {
$store->mark_failure( $action_id );
}
}
/**
* Log the execution of an existing action whether migrated or not.
*
* @param int $action_id Action ID.
*/
public function log_execution( $action_id ) {
$store = $this->get_store_from_action_id( $action_id );
if ( $store ) {
$store->log_execution( $action_id );
}
}
/**
* Mark an existing action complete whether migrated or not.
*
* @param int $action_id Action ID.
*/
public function mark_complete( $action_id ) {
$store = $this->get_store_from_action_id( $action_id );
if ( $store ) {
$store->mark_complete( $action_id );
}
}
/**
* Get an existing action status whether migrated or not.
*
* @param int $action_id Action ID.
*/
public function get_status( $action_id ) {
$store = $this->get_store_from_action_id( $action_id );
if ( $store ) {
return $store->get_status( $action_id );
}
return null;
}
/**
* Return which store an action is stored in.
*
* @param int $action_id ID of the action.
* @param bool $primary_first Optional flag indicating search the primary store first.
* @return ActionScheduler_Store
*/
protected function get_store_from_action_id( $action_id, $primary_first = false ) {
if ( $primary_first ) {
$stores = [
$this->primary_store,
$this->secondary_store,
];
} elseif ( $action_id < $this->demarkation_id ) {
$stores = [
$this->secondary_store,
$this->primary_store,
];
} else {
$stores = [
$this->primary_store,
];
}
foreach ( $stores as $store ) {
$action = $store->fetch_action( $action_id );
if ( ! is_a( $action, 'ActionScheduler_NullAction' ) ) {
return $store;
}
}
return null;
}
/* * * * * * * * * * * * * * * * * * * * * * * * * * *
* All claim-related functions should operate solely
* on the primary store.
* * * * * * * * * * * * * * * * * * * * * * * * * * */
/**
* Get the claim count from the table data store.
*/
public function get_claim_count() {
return $this->primary_store->get_claim_count();
}
/**
* Retrieve the claim ID for an action from the table data store.
*
* @param int $action_id Action ID.
*/
public function get_claim_id( $action_id ) {
return $this->primary_store->get_claim_id( $action_id );
}
/**
* Release a claim in the table data store.
*
* @param ActionScheduler_ActionClaim $claim Claim object.
*/
public function release_claim( ActionScheduler_ActionClaim $claim ) {
$this->primary_store->release_claim( $claim );
}
/**
* Release claims on an action in the table data store.
*
* @param int $action_id Action ID.
*/
public function unclaim_action( $action_id ) {
$this->primary_store->unclaim_action( $action_id );
}
/**
* Retrieve a list of action IDs by claim.
*
* @param int $claim_id Claim ID.
*/
public function find_actions_by_claim_id( $claim_id ) {
return $this->primary_store->find_actions_by_claim_id( $claim_id );
}
}

Some files were not shown because too many files have changed in this diff Show More