Since 1.6.0 "Show" both whitelisted a method for its group and hid it from everybody else. A group shown ordinary methods next to a free one therefore took the ordinary ones away from every stranger, who was left with only the free method. "Only" now carries the reservation; "Show" is just the group's whitelist. Existing show rules are converted to only once on update so the checkout does not change until the shop chooses. Co-Authored-By: Claude Code <[email protected]>
331 lines
9.5 KiB
PHP
331 lines
9.5 KiB
PHP
<?php
|
|
/**
|
|
* Plugin Name: Nolimit Discounts
|
|
* Plugin URI: https://gitea.nolimit.dk/kw/woo-discount-manager
|
|
* Update URI: https://gitea.nolimit.dk/kw/nolimit-discounts-dist
|
|
* Description: Apply discounts and show/hide shipping methods based on the customer's email address (full address, domain or partial match).
|
|
* Version: 1.7.0
|
|
* Author: Kristian Wind
|
|
* Author URI: https://gitea.nolimit.dk/kw
|
|
* License: GPL-2.0-or-later
|
|
* Text Domain: wc-email-discount
|
|
* Requires at least: 5.8
|
|
* Requires PHP: 7.4
|
|
* WC requires at least: 6.0
|
|
* WC tested up to: 11.1.2
|
|
* Requires Plugins: woocommerce
|
|
*/
|
|
|
|
/*
|
|
* "Update URI" is what makes updates work, and it is load-bearing in a second
|
|
* way that is easy to miss: since WordPress 5.8, a plugin that declares one is
|
|
* never asked about on wordpress.org. Remove the line and a .org plugin that
|
|
* happens to share this slug could be offered to every shop running this one.
|
|
* includes/class-updater.php reads it; nothing else configures the source.
|
|
*/
|
|
|
|
if ( ! defined( 'ABSPATH' ) ) {
|
|
exit;
|
|
}
|
|
|
|
define( 'WC_EDM_VERSION', '1.7.0' );
|
|
define( 'WC_EDM_PLUGIN_FILE', __FILE__ );
|
|
define( 'WC_EDM_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
|
|
define( 'WC_EDM_PLUGIN_URL', plugin_dir_url( __FILE__ ) );
|
|
|
|
/**
|
|
* Main plugin class (singleton).
|
|
*/
|
|
final class WC_Email_Discount_Manager {
|
|
|
|
/**
|
|
* Singleton instance.
|
|
*
|
|
* @var WC_Email_Discount_Manager|null
|
|
*/
|
|
private static $instance = null;
|
|
|
|
/**
|
|
* Admin controller.
|
|
*
|
|
* @var WC_EDM_Admin|null
|
|
*/
|
|
public $admin = null;
|
|
|
|
/**
|
|
* Frontend controller.
|
|
*
|
|
* @var WC_EDM_Frontend|null
|
|
*/
|
|
public $frontend = null;
|
|
|
|
/**
|
|
* REST API controller.
|
|
*
|
|
* @var WC_EDM_REST_API|null
|
|
*/
|
|
public $rest_api = null;
|
|
|
|
/**
|
|
* Audit log controller.
|
|
*
|
|
* @var WC_EDM_Audit_Log|null
|
|
*/
|
|
public $audit_log = null;
|
|
|
|
/**
|
|
* Update checker.
|
|
*
|
|
* @var WC_EDM_Updater|null
|
|
*/
|
|
public $updater = null;
|
|
|
|
/**
|
|
* Class files that could not be loaded.
|
|
*
|
|
* A plugin upgrade replaces files one by one, so a request landing
|
|
* mid-copy can see an incomplete plugin folder. Tracking what is missing
|
|
* lets the plugin degrade instead of taking the whole screen down.
|
|
*
|
|
* @var string[]
|
|
*/
|
|
private $missing_files = array();
|
|
|
|
/**
|
|
* Get the singleton instance.
|
|
*
|
|
* @return WC_Email_Discount_Manager
|
|
*/
|
|
public static function instance() {
|
|
if ( null === self::$instance ) {
|
|
self::$instance = new self();
|
|
}
|
|
return self::$instance;
|
|
}
|
|
|
|
/**
|
|
* Constructor. Private — use instance().
|
|
*/
|
|
private function __construct() {
|
|
add_action( 'plugins_loaded', array( $this, 'init' ) );
|
|
add_action( 'before_woocommerce_init', array( $this, 'declare_wc_compatibility' ) );
|
|
}
|
|
|
|
/**
|
|
* Prevent cloning.
|
|
*/
|
|
private function __clone() {}
|
|
|
|
/**
|
|
* Prevent unserializing.
|
|
*/
|
|
public function __wakeup() {
|
|
_doing_it_wrong( __FUNCTION__, 'Unserializing is not allowed.', '1.0.0' );
|
|
}
|
|
|
|
/**
|
|
* The label on the shared parent menu all five plugins register under.
|
|
*
|
|
* A brand name, so it is returned as a literal rather than run through
|
|
* __(): translating it would give the product a different name in Danish.
|
|
*
|
|
* @return string
|
|
*/
|
|
public function shared_menu_title() {
|
|
return 'Nolimit';
|
|
}
|
|
|
|
/**
|
|
* Bootstrap once all plugins are loaded.
|
|
*/
|
|
public function init() {
|
|
// Deliberately before the WooCommerce check. Everything else here
|
|
// needs WooCommerce, but updates do not — and a shop that has
|
|
// WooCommerce switched off for an afternoon must not quietly stop
|
|
// receiving fixes for this plugin.
|
|
$updater = WC_EDM_PLUGIN_DIR . 'includes/class-updater.php';
|
|
if ( is_readable( $updater ) ) {
|
|
require_once $updater;
|
|
$this->updater = new WC_EDM_Updater();
|
|
}
|
|
|
|
// Also deliberately before the WooCommerce check, and for the same
|
|
// reason as the updater above: the shared parent menu is registered by
|
|
// whichever of the five plugins boots first, and its label must not
|
|
// depend on whether THIS plugin got past a gate.
|
|
//
|
|
// Registered here rather than by editing includes/shared-menu.php:
|
|
// that file is byte-identical in all five, and which copy a site loads
|
|
// is decided by activation order. Changing it there would make a site
|
|
// with three of five updated show "Manager" or "Nolimit" depending on
|
|
// who booted first, and change its mind when a plugin is deactivated.
|
|
// The filter is idempotent, so a plugin still on the old build changes
|
|
// nothing.
|
|
add_filter( 'wpmgr_menu_title', array( $this, 'shared_menu_title' ) );
|
|
|
|
if ( ! class_exists( 'WooCommerce' ) ) {
|
|
add_action( 'admin_notices', array( $this, 'woocommerce_missing_notice' ) );
|
|
return;
|
|
}
|
|
|
|
load_plugin_textdomain( 'wc-email-discount', false, dirname( plugin_basename( WC_EDM_PLUGIN_FILE ) ) . '/languages' );
|
|
|
|
$this->includes();
|
|
|
|
if ( ! empty( $this->missing_files ) ) {
|
|
add_action( 'admin_notices', array( $this, 'incomplete_install_notice' ) );
|
|
}
|
|
|
|
// Everything else builds on these two, so without them the plugin
|
|
// stays dormant and shows the notice instead of erroring out.
|
|
if ( ! class_exists( 'WC_EDM_DB' ) || ! class_exists( 'WC_EDM_Helpers' ) ) {
|
|
return;
|
|
}
|
|
|
|
// Upgrade the DB schema after plugin updates that skip re-activation.
|
|
add_action( 'init', array( 'WC_EDM_DB', 'maybe_upgrade' ), 5 );
|
|
|
|
if ( is_admin() && class_exists( 'WC_EDM_Admin' ) ) {
|
|
$this->admin = new WC_EDM_Admin();
|
|
}
|
|
|
|
if ( class_exists( 'WC_EDM_Frontend' ) ) {
|
|
$this->frontend = new WC_EDM_Frontend();
|
|
}
|
|
|
|
if ( class_exists( 'WC_EDM_Audit_Log' ) ) {
|
|
$this->audit_log = new WC_EDM_Audit_Log();
|
|
}
|
|
|
|
if ( class_exists( 'WC_EDM_REST_API' ) ) {
|
|
$this->rest_api = new WC_EDM_REST_API();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Warn that the plugin folder is incomplete.
|
|
*/
|
|
public function incomplete_install_notice() {
|
|
if ( ! current_user_can( 'activate_plugins' ) ) {
|
|
return;
|
|
}
|
|
|
|
echo '<div class="notice notice-error"><p>';
|
|
echo esc_html__( 'Nolimit Discounts could not load all of its files, so some features are unavailable. If you just updated the plugin, reload this page — the upload may still have been in progress. If the warning persists, upload the plugin again.', 'wc-email-discount' );
|
|
echo '</p><p><code>';
|
|
echo implode( '</code>, <code>', array_map( 'esc_html', $this->missing_files ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
|
echo '</code></p></div>';
|
|
}
|
|
|
|
/**
|
|
* Load class files.
|
|
*/
|
|
/**
|
|
* The files loaded only in an admin request.
|
|
*
|
|
* A method rather than a literal inside includes() so a test can read the
|
|
* actual list. The assertion it replaces searched this file's SOURCE TEXT
|
|
* for the shared menu's path, which a comment mentioning that path would
|
|
* have satisfied just as well — the same hollow shape as grepping a file
|
|
* for the words of the guard it is supposed to have.
|
|
*
|
|
* @return string[]
|
|
*/
|
|
public static function admin_includes() {
|
|
return array(
|
|
'includes/shared-menu.php',
|
|
'includes/class-admin.php',
|
|
'includes/class-admin-groups.php',
|
|
'includes/class-admin-emails.php',
|
|
'includes/class-admin-shipping.php',
|
|
'includes/class-admin-audit.php',
|
|
);
|
|
}
|
|
|
|
private function includes() {
|
|
$files = array(
|
|
'includes/class-db.php',
|
|
'includes/class-helpers.php',
|
|
'includes/class-frontend.php',
|
|
'includes/class-audit-log.php',
|
|
'includes/class-rate-limiter.php',
|
|
'includes/class-rest-api.php',
|
|
);
|
|
|
|
if ( is_admin() ) {
|
|
$files = array_merge( $files, self::admin_includes() );
|
|
}
|
|
|
|
foreach ( $files as $file ) {
|
|
$path = WC_EDM_PLUGIN_DIR . $file;
|
|
|
|
// A missing file is recorded rather than fatal, so a half-copied
|
|
// upgrade cannot take down wp-admin.
|
|
if ( is_readable( $path ) ) {
|
|
require_once $path;
|
|
} else {
|
|
$this->missing_files[] = $file;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Declare compatibility with WooCommerce HPOS.
|
|
*/
|
|
public function declare_wc_compatibility() {
|
|
if ( class_exists( '\Automattic\WooCommerce\Utilities\FeaturesUtil' ) ) {
|
|
\Automattic\WooCommerce\Utilities\FeaturesUtil::declare_compatibility( 'custom_order_tables', WC_EDM_PLUGIN_FILE, true );
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Admin notice shown when WooCommerce is not active.
|
|
*/
|
|
public function woocommerce_missing_notice() {
|
|
echo '<div class="notice notice-error"><p>';
|
|
esc_html_e( 'Nolimit Discounts requires WooCommerce to be installed and active.', 'wc-email-discount' );
|
|
echo '</p></div>';
|
|
}
|
|
|
|
/**
|
|
* Plugin activation: verify WooCommerce and create database tables.
|
|
*/
|
|
public static function activate() {
|
|
$active_plugins = apply_filters( 'active_plugins', get_option( 'active_plugins', array() ) );
|
|
|
|
if ( ! class_exists( 'WooCommerce' ) && ! in_array( 'woocommerce/woocommerce.php', $active_plugins, true ) ) {
|
|
deactivate_plugins( plugin_basename( WC_EDM_PLUGIN_FILE ) );
|
|
wp_die(
|
|
esc_html__( 'Nolimit Discounts requires WooCommerce to be installed and active. Please activate WooCommerce first.', 'wc-email-discount' ),
|
|
esc_html__( 'Plugin activation error', 'wc-email-discount' ),
|
|
array( 'back_link' => true )
|
|
);
|
|
}
|
|
|
|
require_once WC_EDM_PLUGIN_DIR . 'includes/class-db.php';
|
|
WC_EDM_DB::create_tables();
|
|
}
|
|
|
|
/**
|
|
* Plugin deactivation: clear scheduled jobs. Data is left untouched.
|
|
*/
|
|
public static function deactivate() {
|
|
require_once WC_EDM_PLUGIN_DIR . 'includes/class-audit-log.php';
|
|
WC_EDM_Audit_Log::unschedule_prune();
|
|
}
|
|
}
|
|
|
|
register_activation_hook( __FILE__, array( 'WC_Email_Discount_Manager', 'activate' ) );
|
|
register_deactivation_hook( __FILE__, array( 'WC_Email_Discount_Manager', 'deactivate' ) );
|
|
|
|
/**
|
|
* Access the main plugin instance.
|
|
*
|
|
* @return WC_Email_Discount_Manager
|
|
*/
|
|
function wc_edm() {
|
|
return WC_Email_Discount_Manager::instance();
|
|
}
|
|
|
|
wc_edm();
|