Kristian named the five plugins as a family, and three repos disagreed with what
he calls them. His decision: rename the repo, never the installed slug.
MEASURED BEFORE DOING IT, because four published releases carry the old URL in
their Update URI header. A throwaway repo was created, renamed and deleted:
old API path 301 -> new
old web path 301 -> new
old /releases/download/... path 301 -> new
and WordPress follows up to 5 redirects by default (class-wp-http.php:191), on
GET. Confirmed afterwards against the real thing: the old feed URL resolves to
v1.4.5 and the old download path still serves 48185 bytes.
So a shop on 1.4.2 keeps updating through the redirect, and 1.4.6 moves it onto
the new address directly.
PLUGIN_SLUG and PLUGIN_MAIN are deliberately untouched and now disagree with the
repository name. That is permanent: the installed directory has been
woocommerce-email-discount-manager since the first release and is on a live
shop, and changing it would not move that installation — it would put a SECOND
copy beside it and leave the old one active. ci/plugin.conf says so in the file,
because otherwise somebody will helpfully align them.
Also in, both for the family rather than for this plugin alone:
- docs/REUSE.md gains the hostname-keyed hook as a first-class section. It is
the only bug we have found that is CAUSED by there being more than one plugin,
so it is invisible to anyone reading a single repo — along with the same shape
in a shared-*.php file copied into five plugins, which fatals with "Cannot
redeclare" on the second activation without function_exists() guards.
- ci/static.sh fails on anything shaped like a credential in a tracked file.
Suggested by the AI-manager port: a pushed key cannot be unpushed, only
rotated. Deliberately narrow — sk-ant-, sk-, ghp_, gho_, github_pat_, AKIA,
PRIVATE KEY blocks — because a broad entropy heuristic fires on minified
assets and a check that cries wolf gets commented out.
Verified failing before being believed: a file containing sk-ant-api03-... makes
the step report the filename and the suite exit 1.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
292 lines
8.0 KiB
PHP
292 lines
8.0 KiB
PHP
<?php
|
|
/**
|
|
* Plugin Name: WooCommerce Email Discount Manager
|
|
* Plugin URI: https://gitea.nolimit.dk/kw/woo-discount-manager
|
|
* Update URI: https://gitea.nolimit.dk/kw/woo-discount-manager
|
|
* Description: Apply discounts and show/hide shipping methods based on the customer's email address (full address, domain or partial match).
|
|
* Version: 1.4.6
|
|
* 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.0
|
|
* 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.4.6' );
|
|
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' );
|
|
}
|
|
|
|
/**
|
|
* 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();
|
|
}
|
|
|
|
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(
|
|
'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();
|