2026-08-23 15:38:14 +02:00
<? php
/**
2026-09-17 22:05:19 +02:00
* Plugin Name: Nolimit Discounts
2026-09-16 18:18:50 +02:00
* Plugin URI: https://gitea.nolimit.dk/kw/woo-discount-manager
2026-09-17 22:38:12 +02:00
* Update URI: https://gitea.nolimit.dk/kw/nolimit-discounts-dist
2026-08-23 15:38:14 +02:00
* Description: Apply discounts and show/hide shipping methods based on the customer's email address (full address, domain or partial match).
2026-09-20 07:52:01 +02:00
* Version: 1.5.0
2026-09-16 12:59:44 +02:00
* Author: Kristian Wind
* Author URI: https://gitea.nolimit.dk/kw
2026-08-23 15:38:14 +02:00
* 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
2026-09-16 10:34:38 +02:00
* WC tested up to: 11.1.0
2026-08-23 15:38:14 +02:00
* Requires Plugins: woocommerce
*/
2026-09-16 10:34:38 +02:00
/*
* "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.
*/
2026-08-23 15:38:14 +02:00
if ( ! defined ( 'ABSPATH' ) ) {
exit ;
}
2026-09-20 07:52:01 +02:00
define ( 'WC_EDM_VERSION' , '1.5.0' );
2026-08-23 15:38:14 +02:00
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 ;
2026-09-16 10:34:38 +02:00
/**
* Update checker.
*
* @var WC_EDM_Updater|null
*/
public $updater = null ;
2026-08-23 15:38:14 +02:00
/**
* 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' );
}
2026-09-17 22:05:19 +02:00
/**
* 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' ;
}
2026-08-23 15:38:14 +02:00
/**
* Bootstrap once all plugins are loaded.
*/
public function init () {
2026-09-16 10:34:38 +02:00
// 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 ();
}
2026-09-17 22:05:19 +02:00
// 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' ) );
2026-08-23 15:38:14 +02:00
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__ ( 'WooCommerce Email Discount Manager 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.
*/
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 ,
array (
2026-09-17 22:05:19 +02:00
'includes/shared-menu.php' ,
2026-08-23 15:38:14 +02:00
'includes/class-admin.php' ,
'includes/class-admin-groups.php' ,
'includes/class-admin-emails.php' ,
'includes/class-admin-shipping.php' ,
'includes/class-admin-audit.php' ,
)
);
}
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 ( 'WooCommerce Email Discount Manager 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__ ( 'WooCommerce Email Discount Manager 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 ();