Files
Dapper/dapper.php
carpentryplus25 1e5b21221f
All checks were successful
Generate Build Info / build-info (push) Successful in 2s
Contact Form 7 Syntax error
2026-03-10 23:01:18 -04:00

1958 lines
80 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/*
Plugin Name: Dapper
Description: Lightweight WooCommerce utility for automated order emails, checkout/registration anti-spam (honeypot + JS + speed checks), custom email templates & campaigns, mailing list management, and custom WP-admin login logo branding. Future: robust automated site backups & restores. Ideal for managed hosting and reliable WooCommerce stores — no third-party dependencies.
Version: 1.1.1
Author: William Thompson
Plugin URI: https://theblueduckpond.com
Author URI: https://eastofthehaw.com
Text Domain: dapper
Domain Path: /languages
* Changelog: See readme.txt (short) and CHANGELOG.md (full)
*/
// Exit if accesses directly
if ( ! defined('ABSPATH')) {
exit;
}
// Plugin constants
define( 'DAPPER_VERSION', '1.1.1' );
define( 'DAPPER_PATH', plugin_dir_path( __FILE__ ) );
define( 'DAPPER_URL', plugin_dir_url( __FILE__ ) );
define( 'DAPPER_BASENAME', plugin_basename( __FILE__ ) );
// Include statements
include "dapper_backup.php";
/*
// Runs when the activation hook gets called
// when the plugin first get activated
*/
function dapper_activation() {
if (!get_option('dapper_setup_complete')) {
// Running setup tasks
create_mailing_list_table();// function to create the dapper_mailing_list table
$existing_users = get_users();// Get the user table array
// Loop through the existing users and add them to the mailing list
foreach ($existing_users as $user) {
update_mailing_list($user->ID, $user->user_email, 'subscribed');
}
// Depreciated in v1.1.1 to be removed in later versions
// Creates the 'images' folder when plugin gets activated
//make_images_dir();
make_backup_dir();
// Mark setup completed
update_option('dapper_setup_complete', true);
}
}
register_activation_hook(__FILE__, 'dapper_activation');
/**
* Get build identifier from generated build-info.php (CI auto-generated)
* Fallbacks: local git (dev only), then 'dev-local'
*
* @return string Short commit hash or descriptive fallback
*/
function dapper_get_build_identifier() {
static $build = null;
if ( $build !== null ) {
return $build;
}
$info_file = DAPPER_PATH . 'dapper/build-info.php';
if ( file_exists( $info_file ) && is_readable( $info_file ) ) {
$info = include $info_file;
if ( is_array( $info ) && ! empty( $info['commit'] ) && preg_match( '/^[0-9a-f]{7,40}$/', $info['commit'] ) ) {
$build = substr( $info['commit'], 0, 8 ); // enforce short
if ( ! empty( $info['branch'] ) && $info['branch'] !== 'main' && $info['branch'] !== 'master' ) {
$build .= ' (' . $info['branch'] . ')';
}
if ( ! empty( $info['built'] ) ) {
$build .= ' built ' . $info['built'];
}
return $build;
}
}
// Optional: local dev fallback if you're working without CI yet
$git_dir = DAPPER_PATH . '.git';
if ( is_dir( $git_dir ) ) {
// Your original git-reading logic here as secondary fallback...
// (keep it if you want, but comment out in production deploys)
}
$build = 'dev-local';
return $build;
}
define( 'DAPPER_BUILD', dapper_get_build_identifier() );
/*
// Depreciated in v1.1.1 to be removed in later versions
// Checks for an images directory inside the dapper plugin
// Creates the 'images' folder when plugin gets activated
// Used for custom logos on login page
*/
function make_images_dir() {
$dapper_root = plugin_dir_path(__FILE__);
$image_dir = $dapper_root . '/images';
if (!file_exists($image_dir)){
wp_mkdir_p($image_dir);
}
}
/*
// Adds users to the dapper_mailing _list initially
// Also manages the mailing list
// Has three required arguments
// @param $user_id integer id of user in mailing list
// @param $email string email of user
// @param $sub_status string 'subscribed' or 'unsubscribed'
*/
function update_mailing_list($user_id, $email, $sub_status) {
global $wpdb;
$table_name = $wpdb->prefix . 'dapper_mailing_list';
// Check if the user is already in the mailing list
$existing_user = $wpdb->get_row($wpdb->prepare("SELECT * FROM $table_name WHERE user_id = %d", $user_id));
if (!$existing_user) {
// If the user is not in the list, insert the new record
$wpdb->insert(
$table_name,
array(
'user_id' => $user_id,
'email' => $email,
'subscription_status' => $sub_status,
),
array('%d', '%s', '%s')
);
} else {
// If the user is already in the list, update the subscription status
$wpdb->update(
$table_name,
array('subscription_status' => $sub_status),
array('user_id' => $user_id),
array('%s'),
array('%d')
);
}
}
/*
// Called during intial run as part of the setup process
// Generates the database table for the mailing list
*/
function create_mailing_list_table() {
global $wpdb;
$table_name = $wpdb->prefix . 'dapper_mailing_list';
$charset_collate = $wpdb->get_charset_collate();
$sql = "CREATE TABLE $table_name (
id mediumint(9) NOT NULL AUTO_INCREMENT,
user_id mediumint(9) NOT NULL,
email varchar(255) NOT NULL,
subscription_status varchar(20) NOT NULL,
PRIMARY KEY (id)
) $charset_collate;";
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
dbDelta($sql);
}
/*
// Get the total number of mailing list users
// Returns an Integer
*/
function get_mailing_list_users() {
global $wpdb;
$table_name = $wpdb->prefix . 'dapper_mailing_list'; // Assuming your table has a WordPress prefix
// Fetch users from the mailing list table
$query = "SELECT * FROM $table_name";
$results = $wpdb->get_results($query);
return $results;
}
/*
// Removes a user from the dapper mailing list
// Called from unsubscribe links asking for removal
// Called when a user is manually removed from the mailing list
// @param $user_id integer id of the passed in user required
*/
function remove_user_from_mailing_list($user_id) {
global $wpdb;
$table_name = $wpdb->prefix . 'dapper_mailing_list';
// Remove the user from the mailing list
$wpdb->delete($table_name, array('user_id' => $user_id));
}
// Create Email Template post type
// Todo: Should probably move to some sort of init plugin function
function create_email_template_post_type() {
$labels = array(
'name' => __('Dapper'),
'singular_name' => __('Dapper'),
'add_new' => __('Add New Template'),
'add_new_item' => __('Add New Email Template'),
'edit_item' => __('Edit Email Template'),
'new_item' => __('New Email Template'),
'view_item' => __('View Email Template'),
'view_items' => __('View Email Templates'),
'search_items' => __('Search Email Templates'),
'not_found' => __('No Email Templates found'),
'not_found_in_trash' => __('No Email Templates found in Trash'),
'parent_item_colon' => '',
'all_items' => __('All Email Templates'),
'archives' => __('Email Template Archives'),
'attributes' => __('Email Template Attributes'),
'insert_into_item' => __('Insert into email template'),
'uploaded_to_this_item' => __('Uploaded to this email template'),
'featured_image' => _x('Featured Image', 'email template'),
'set_featured_image' => _x('Set featured image', 'email template'),
'remove_featured_image' => _x('Remove featured image', 'email template'),
'use_featured_image' => _x('Use as featured image', 'email template'),
'menu_name' => __('Dapper'),
'filter_items_list' => __('Filter email templates list'),
'items_list_navigation' => __('Email Templates list navigation'),
'items_list' => __('Email Templates list'),
'item_published' => __('Email Template published'),
'item_published_privately' => __('Email Template published privately'),
'item_reverted_to_draft' => __('Email Template reverted to draft'),
'item_scheduled' => __('Email Template scheduled'),
'item_updated' => __('Email Template updated'),
);
$args = array(
'labels' => $labels,
'public' => true,
'has_archive' => true,
'supports' => array('title', 'editor'),
'menu_icon' => 'dashicons-email-alt',
);
register_post_type('email_template', $args);
}
add_action('init', 'create_email_template_post_type');
// Create Campaign post type
// Todo: Should most likly be in some sort of init plugin function
function create_campaign_post_type() {
register_post_type('email_campaign',
array(
'labels' => array(
'name' => __('Email Campaigns'),
'singular_name' => __('Email Campaign')
),
'public' => true,
'has_archive' => true,
'supports' => array('title', 'editor'),
'show_in_menu' => false,
)
);
}
add_action('init', 'create_campaign_post_type');
// Useful for sending out daily newsletters
function get_latest_email_template_id() {
// Query to get the latest email template post
$args = array(
'post_type' => 'email_template',
'posts_per_page' => 1,
'order' => 'DESC',
'orderby' => 'date',
);
$template_query = new WP_Query($args);
if ($template_query->have_posts()) {
$template_query->the_post();
return get_the_ID(); // Return the ID of the latest email template post
}
return 0; // Return 0 if no template is found
}
/*
// Actual email sending function calls wp_mail()
// Requires three arguments:
// @param string $to; the email recipient
// @param string $subject; what the email is about
// @param string $message; the body of the message to be sent can be html with embedded php.
// Requires a working email server in conjuntion with webserver
*/
function send_custom_email($to, $subject, $message) {
//$headers = array('Content-Type: text/html; charset=UTF-8');
// Additional headers if needed
$headers = get_option('dapper_headers', '');
// Send the email
wp_mail($to, $subject, $message, $headers);
}
// Define WooCommerce order statuses
$order_statuses = array(
'completed' => 'completed',
'shipped' => 'shipped',
'delivered' => 'delivered',
'pending_payment' => 'pending',
'failed' => 'failed',
'draft' => 'checkout-draft',
'cancelled' => 'cancelled',
'refunded' => 'refunded',
'processing' => 'processing',
'on_hold' => 'on-hold',
'partial_shipped' => 'partial-shipped'
// Add more statuses as needed
);
function get_possible_order_statuses() {
$order_statuses = array(
'wc-pending' => _x( 'Pending payment', 'Order status', 'woocommerce' ),
'wc-processing' => _x( 'Processing', 'Order status', 'woocommerce' ),
'wc-on-hold' => _x( 'On hold', 'Order status', 'woocommerce' ),
'wc-completed' => _x( 'Completed', 'Order status', 'woocommerce' ),
'wc-cancelled' => _x( 'Cancelled', 'Order status', 'woocommerce' ),
'wc-refunded' => _x( 'Refunded', 'Order status', 'woocommerce' ),
'wc-failed' => _x( 'Failed', 'Order status', 'woocommerce' ),
);
return $order_statuses;
}
/**
* Conditionally load WooCommerce-dependent features. Modulized use.
*/
function dapper_woo_conditional_load() {
// Only proceed if WooCommerce exists AND toggle is enabled
if ( ! class_exists( 'WooCommerce' ) || get_option( 'dapper_enable_woo_integration', 'on' ) !== 'on' ) {
return;
}
// Called by the WooCommerce order delivered webhook when an order is marked delivered
// Useful for sending out review links
// Todo: make review template and set this function up for that use.
function send_custom_email_on_order_completion($order_id) {
// Get the order object
$order = wc_get_order($order_id);
// Get the customer email from the order
$to = $order->get_billing_email();
// Get the name of the email template controlled in settings
$template_name = get_option('dapper_woo_status_delivered', '');
// Get the email template content
$message = get_email_template_content_by_name($template_name);
// Replace placeholders with actual data
$message = str_replace('[CustomerName]', $order->get_billing_first_name(), $message);
$message = str_replace('[OrderNumber]', $order->get_order_number(), $message);
// Get product names, quantities, and order total
$product_info = array();
$total_quantity = 0;
foreach ( $order->get_items() as $item_id => $item ) {
$product = $item->get_product();
$quantity = $item->get_quantity();
$total_quantity += $quantity;
// Guard against deleted/missing products
if ( $product && is_object( $product ) && method_exists( $product, 'get_name' ) ) {
$product_name = $product->get_name();
$original_product_slug = $product->get_slug();
$product_info[] = "$product_name (Qty: $quantity)";
// Slug cleanup and review link only if product exists
$product_slug = preg_replace( '/-(s|m|l|xl|2xl|3xl|4xl|5xl)$/i', '', $original_product_slug );
$review_link = get_option( 'dapper_review_link_structure', '' );
$review_link .= "/$product_slug/#reviews";
$product_review_link = $review_link;
$message = str_replace( "[ReviewLink_0]", $product_review_link, $message );
} else {
// Fallback for missing product — still count quantity, use generic name
$product_info[] = "Unknown Product (Qty: $quantity)";
dapper_debug_log( "Missing/deleted product in order #$order_id, item $item_id" );
// Note: No review link added for this item
}
}
// Get the numeric order total without HTML or currency symbols
$order_total_numeric = get_order_total_numeric($order_id);
// Replace [ProductName], [Quantity], [OrderTotal] with actual data
$message = str_replace('[ProductName]', implode(', ', $product_info), $message);
$message = str_replace('[Quantity]', $total_quantity, $message);
$message = str_replace('[OrderTotal]', $order_total_numeric, $message);
// Replace [ReviewLink_*] placeholders with actual review links in your email template
$message = preg_replace('/\[ReviewLink_(\d+)\]/', '[ReviewLink_$1]', $message);
// Add more replacements as needed
// Check if the email and template content exist
if ($to && $message) {
$subject = 'Order Delivered';
// Send the email
send_custom_email($to, $subject, $message);
}
}
/* Called by WooCommerce order status changed webhook callback when an order is marked shipped
// Useful for sending the tracking info to the customer requires
// Advanced Shipment Tracking for WooCommerce plugin otherwise tracking is null
*/
function send_custom_email_on_order_shipped($order_id) {
// Get the order object
$order = wc_get_order($order_id);
// Get the customer email from the order
$to = $order->get_billing_email();
// Get the name of the email template controlled in settings
$template_name = get_option('dapper_woo_status_shipped', '');
// Get the email template content
$message = get_email_template_content_by_name($template_name);
// Replace placeholders with actual data
$message = str_replace('[CustomerName]', $order->get_billing_first_name(), $message);
$message = str_replace('[OrderNumber]', $order->get_order_number(), $message);
// Get product names, quantities, and order total
$product_info = array();
$total_quantity = 0;
foreach ( $order->get_items() as $item_id => $item ) {
$product = $item->get_product();
$quantity = $item->get_quantity();
$total_quantity += $quantity;
if ( $product && is_object( $product ) && method_exists( $product, 'get_name' ) ) {
$product_name = $product->get_name();
$product_info[] = "$product_name (Qty: $quantity)";
} else {
$product_info[] = "Unknown Product (Qty: $quantity)";
dapper_debug_log( "Missing/deleted product in order #$order_id, item $item_id" );
}
}
// Get the numeric order total without HTML or currency symbols
$order_total_numeric = get_order_total_numeric($order_id);
// Replace [ProductName], [Quantity], [OrderTotal] with actual data
$message = str_replace('[ProductName]', implode(', ', $product_info), $message);
$message = str_replace('[Quantity]', $total_quantity, $message);
$message = str_replace('[OrderTotal]', $order_total_numeric, $message);
$order_notes = $order->get_customer_order_notes();
// Check if function exist
if ( function_exists( 'ast_get_tracking_items' ) ) {
$tracking_items = ast_get_tracking_items($order_id);
foreach($tracking_items as $tracking_item){
$tracking_number = $tracking_item['tracking_number'];
$tracking_provider = $tracking_item['formatted_tracking_provider'];
$tracking_link = $tracking_item['formatted_tracking_link'];
$date_shipped = date_i18n( get_option( 'date_format' ), $tracking_item['date_shipped'] );
if (!$tracking_number) {
dapper_debug_log('Tracking number is blank/null/empty');
}
if ($tracking_number) {
dapper_debug_log('Tracking number is:' . $tracking_number);
$message = str_replace('[TrackingNumber]', $tracking_number, $message);
$message = str_replace('[TrackingLink]', $tracking_link, $message);
}
}
} else {
$message = str_replace('[TrackingNumber]', '', $message);
$message = str_replace('[TrackingLink]', '', $message);
}
// Check if the email and template content exist
if ($to && $message) {
$subject = 'Order Shipped';
// Send the email
send_custom_email($to, $subject, $message);
}
}
;
/*
// WooCommerce Order Status Changed Webhook Callback function
// Handles the switch betweent different order statuses
// Has three required arguments
// @param $order_id integer the passed in Order
// @param $old_status string the passed in old order status
// @param $new_status string the passed in new order status
*/
function handle_woocommerce_order_status_changed($order_id, $old_status, $new_status) {
global $order_statuses;
// Debugging statements
dapper_debug_log('New Status: ' . $new_status);
dapper_debug_log('Order Statuses: ' . print_r($order_statuses, true));
dapper_debug_log( "Order status changed: Order #$order_id from '$old_status' to '$new_status'" );
// Check if the new status is in our defined statuses
if (in_array($new_status, $order_statuses)) {
// Perform actions based on the order status
switch ($new_status) {
case $order_statuses['delivered']:
send_custom_email_on_order_completion($order_id);
dapper_debug_log('Case 1: Delivered');
break;
case $order_statuses['completed']:
send_custom_email_on_order_shipped($order_id);
dapper_debug_log('Case 2: Shipped(which is completed status actually)');
break;
case $order_statuses['pending_payment']:
dapper_debug_log('Case 3: Pending Payment');
break;
case $order_statuses['processing']:
dapper_debug_log('Case 4: Processing');
break;
case $order_statuses['on_hold']:
dapper_debug_log('Case 5: On Hold');
break;
case $order_statuses['partial_shipped']:
dapper_debug_log('Case 6: Partially Shipped');
break;
case $order_statuses['cancelled']:
dapper_debug_log('Case 7: Cancelled');
break;
case $order_statuses['refunded']:
dapper_debug_log('Case 8: Refunded');
break;
case $order_statuses['failed']:
dapper_debug_log('Case 9: Failed');
break;
case $order_statuses['draft']:
dapper_debug_log('Case 10: Draft');
break;
default:
dapper_debug_log('Unknown Status: ' . $new_status);
// Add more cases for other statuses if needed
}
}
}
// Hook the status changed action
add_action( 'woocommerce_order_status_changed', 'handle_woocommerce_order_status_changed', 10, 3 );
// Honeypot only load if Woo + toggle on
add_action( 'woocommerce_register_form_start', 'advanced_add_honeypots' );
add_filter( 'woocommerce_registration_errors', 'advanced_validate_registration', 10, 3 );
// Define the honeypot functions inside too (or keep them global if you prefer)
function advanced_add_honeypots() {
$random_name = 'hp_' . substr(str_shuffle('abcdefghijklmnopqrstuvwxyz'), 0, 8); // Random field name
?>
<input type="hidden" name="form_timestamp" value="<?php echo time(); ?>">
<p style="position:absolute; left:-9999px; top:-9999px;">
<input type="text" name="<?php echo esc_attr($random_name); ?>" value="" autocomplete="off" tabindex="-1">
</p>
<?php
}
function advanced_validate_registration($errors, $username, $email) {
// Existing honeypot check...
foreach ($_POST as $key => $value) {
if (strpos($key, 'hp_') === 0 && !empty($value)) {
$errors->add('honeypot_error', __('Spam detected. Registration blocked.', 'woocommerce'));
return $errors;
}
}
// Existing timestamp check...
if (isset($_POST['form_timestamp']) && (time() - intval($_POST['form_timestamp']) < 5)) {
$errors->add('timestamp_error', __('Submission too fast. Humans only.', 'woocommerce'));
}
// === NEW ENHANCED EMAIL PATTERN BLOCK ===
// Target common bot patterns: pure alphanumeric usernames 8+ chars, no dots/hyphens/underscores/numbers mixed naturally
$email_lower = strtolower($email);
$local_part = substr($email, 0, strpos($email, '@'));
// List of domains the bots are currently using
$suspicious_domains = array('hotmail.com', 'outlook.com', 'protonmail.com', 'gmail.com', 'yahoo.com'); // Add more if they switch
$domain = substr(strrchr($email_lower, "@"), 1);
if (in_array($domain, $suspicious_domains)) {
// Pattern: only letters/numbers, 8+ characters, NO dots, hyphens, underscores, or other separators
if (preg_match('/^[a-zA-Z0-9]{8,}$/', $local_part) &&
!str_contains($local_part, '.') &&
!str_contains($local_part, '-') &&
!str_contains($local_part, '_')) {
$errors->add('email_pattern_error', __('Invalid email address. Please use a real, personal email.', 'woocommerce'));
return $errors; // Early exit once blocked
}
}
// === END NEW BLOCK ===
return $errors;
}
/**
* Returns true if the current posted payment method is any known PayPal variant
*
*
*/
function dapper_is_paypal_payment_method() {
if (!isset($_POST['payment_method'])) {
return false;
}
$method = $_POST['payment_method'];
$paypal_methods = [
'paypal',
'ppec_paypal', // older PayPal Express Checkout
'ppcp-gateway', // PayPal Payments (most common now)
'paypal_express',
'braintree_paypal', // if using Braintree integration
// add more here if you ever see other variants in logs
];
return in_array($method, $paypal_methods, true);
}
// ===================
// CHECKOUT HONEYPOT + ANTI-BOT LAYERS
// ===================
// Add hidden fields to checkout form
add_action('woocommerce_checkout_before_customer_details', 'dapper_checkout_honeypot_fields');
function dapper_checkout_honeypot_fields() {
// Hidden honeypot field — bots tend to fill all inputs
$honeypot_name = 'dapper_hp_' . wp_generate_password(8, false); // random per load
?>
<div style="position:absolute; left:-9999px; top:-9999px; height:0; overflow:hidden; opacity:0;">
<input type="text" name="<?php echo esc_attr($honeypot_name); ?>" value="" autocomplete="off" tabindex="-1" />
<!-- Second trap: field that should remain empty unless JS runs -->
<input type="hidden" name="dapper_js_check" value="" />
</div>
<!-- Timestamp to block very fast submissions -->
<input type="hidden" name="dapper_form_time" value="<?php echo time(); ?>" />
<?php
}
// Validate on checkout
add_action('woocommerce_after_checkout_validation', 'dapper_validate_checkout_anti_spam', 10, 2);
function dapper_validate_checkout_anti_spam($data, $errors) {
// Honeypot trap: if any field starting with dapper_hp_ has a value → bot
foreach ($_POST as $key => $value) {
if (strpos($key, 'dapper_hp_') === 0 && !empty(trim($value))) {
$errors->add('dapper_honeypot_error', __('Sorry, we detected automated behavior. Please try again.', 'dapper'));
return;
}
}
// JS check: add tiny JS that sets the field (bots often ignore <script>)
// You can enqueue a tiny JS file or inline it
// For now, if dapper_js_check is still empty → likely no JS → possible bot
if (empty($_POST['dapper_js_check'])) {
// Optional: make this warning only, or block if spam is very high
// $errors->add('dapper_js_error', __('JavaScript required for checkout. Please enable it.', 'dapper'));
// For now just log — tighten later if needed
dapper_debug_log("Checkout submission without JS check - possible bot - Order data: " . print_r($data, true));
}
// Timestamp: block submissions < 6 seconds (bots are fast)
if (isset($_POST['dapper_form_time'])) {
$time_diff = time() - intval($_POST['dapper_form_time']);
if ($time_diff < 6) {
$errors->add('dapper_speed_error', __('Submission too fast — please try again.', 'dapper'));
}
}
}
add_action('wp_footer', 'dapper_checkout_js_check', 100);
function dapper_checkout_js_check() {
if (is_checkout()) {
?>
<script>
document.addEventListener('DOMContentLoaded', function() {
var jsField = document.querySelector('input[name="dapper_js_check"]');
if (jsField) jsField.value = 'human_' + Math.random().toString(36).substring(2);
});
</script>
<?php
}
}
// Add custom human verification before PayPal submission
add_action('woocommerce_checkout_before_customer_details', 'dapper_add_custom_human_check');
function dapper_add_custom_human_check() {
if (get_option('dapper_enable_paypal_human_check', 'on') !== 'on') {
return;
}
if ( ! dapper_is_paypal_payment_method() && ! is_checkout() ) {
return;
}
?>
<div id="dapper-human-check" style="margin:20px 0; padding:15px; background:#f8f9fa; border:1px solid #ddd; border-radius:4px; display:none;">
<strong>Verify you're human to use PayPal</strong><br>
<label><input type="checkbox" id="dapper_human_checkbox"> I'm not a robot</label>
<input type="hidden" name="dapper_human_token" id="dapper_human_token" value="">
<input type="hidden" name="dapper_human_time" id="dapper_human_time" value="<?php echo time(); ?>">
<p class="description" style="color:#555; margin-top:8px;">This helps protect against automated spam. Takes 2 seconds.</p>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
const paypalMethods = ['paypal', 'ppec_paypal', 'ppcp-gateway', 'paypal_express', 'braintree_paypal'];
const checkDiv = document.getElementById('dapper-human-check');
// Function to show/hide based on current selected method
function updateHumanCheckVisibility() {
const selectedRadio = document.querySelector('input[name="payment_method"]:checked');
if (selectedRadio && paypalMethods.includes(selectedRadio.value)) {
checkDiv.style.display = 'block';
} else {
checkDiv.style.display = 'none';
}
}
// Run immediately on load
updateHumanCheckVisibility();
// Listen for changes on payment radios
document.querySelectorAll('input[name="payment_method"]').forEach(radio => {
radio.addEventListener('change', updateHumanCheckVisibility);
});
// Watch for dynamic payment section updates (WooCommerce often reloads parts via AJAX)
const paymentBox = document.querySelector('#payment') || document.body;
const observer = new MutationObserver(updateHumanCheckVisibility);
observer.observe(paymentBox, { childList: true, subtree: true });
// Token generation
const checkbox = document.getElementById('dapper_human_checkbox');
if (checkbox) {
checkbox.addEventListener('change', function() {
const tokenField = document.getElementById('dapper_human_token');
tokenField.value = this.checked
? 'human_' + Math.random().toString(36).substring(2) + '_' + Date.now()
: '';
});
}
});
</script>
<?php
}
// Validate on checkout
add_action('woocommerce_after_checkout_validation', 'dapper_validate_custom_human_check', 15, 2);
function dapper_validate_custom_human_check($data, $errors) {
if (!dapper_is_paypal_payment_method()) {
return;
}
$token = trim( $_POST['dapper_human_token'] ?? '' );
$time = (int) ($_POST['dapper_human_time'] ?? 0);
if (empty($token)) {
$errors->add('human_check_error', __('Please check the "I\'m not a robot" box to continue with PayPal.', 'dapper'));
return;
}
if (time() - $time > 300) {
$errors->add('human_check_error', __('Verification timed out. Please refresh and try again.', 'dapper'));
return;
}
if (strpos($token, 'human_') !== 0 || strlen($token) < 20) {
$errors->add('human_check_error', __('Invalid verification. Please try again.', 'dapper'));
}
}
// 1. Checkbox on SINGLE PRODUCT pages (near PayPal button)
add_action('woocommerce_paypal_payments_single_product_button', 'dapper_human_checkbox_product', 5);
// Product page checkbox - use core Woo hook + JS to force show near Add to Cart / PayPal area
add_action('woocommerce_after_add_to_cart_button', 'dapper_human_checkbox_product', 999); // High priority = late output
function dapper_human_checkbox_product() {
dapper_debug_log('Universal product checkbox output attempt - Product ID: ' . get_the_ID());
if (get_option('dapper_enable_paypal_human_check', 'on') !== 'on') {
return;
}
global $product;
if (!$product || !$product->is_purchasable()) return;
?>
<div id="dapper-human-check-product" style="margin:20px 0; padding:15px; background:#fff3cd; border:1px solid #ffeeba; border-radius:4px; font-weight:bold;">
<strong>Human Verification Required</strong><br>
<label style="display:block; margin:12px 0; font-size:1.1em;">
<input type="checkbox" id="dapper_human_checkbox_product" required style="transform:scale(1.6); margin-right:12px; vertical-align:middle;">
Confirm I'm not a robot
</label>
<p style="color:#856404; font-size:0.95em;">This prevents automated spam.</p>
</div>
<script>
(function() {
console.log('Universal Dapper product protection initialized');
const CHECKBOX_ID = 'dapper_human_checkbox_product';
const CHECKBOX_CONTAINER_ID = 'dapper-human-check-product';
let observer = null;
function disablePayPalButtons() {
// Target common PayPal Smart Button patterns universally
document.querySelectorAll(
'[id*="paypal"], [class*="paypal"], [data-paypal], [data-pp], ' +
'iframe[src*="paypal.com"], div[role="button"][aria-label*="PayPal"], ' +
'button[aria-label*="Pay with PayPal"], .pp-button, .smart-button'
).forEach(el => {
// Strong disable: remove pointer events + overlay
el.style.pointerEvents = 'none';
el.style.userSelect = 'none';
el.style.opacity = '0.45';
el.style.cursor = 'not-allowed';
el.style.position = 'relative';
// Add overlay if missing
if (!el.querySelector('.dapper-universal-overlay')) {
const overlay = document.createElement('div');
overlay.className = 'dapper-universal-overlay';
overlay.style.position = 'absolute';
overlay.style.inset = '0';
overlay.style.background = 'rgba(0,0,0,0.3)';
overlay.style.display = 'flex';
overlay.style.alignItems = 'center';
overlay.style.justifyContent = 'center';
overlay.style.color = '#fff';
overlay.style.fontWeight = 'bold';
overlay.style.fontSize = '1.3em';
overlay.style.textAlign = 'center';
overlay.style.padding = '15px';
overlay.style.zIndex = '10';
overlay.innerText = 'Check verification box above to enable PayPal';
el.appendChild(overlay);
}
});
}
function enablePayPalButtons() {
document.querySelectorAll('.dapper-universal-overlay').forEach(overlay => {
overlay.remove();
});
// Re-enable (PayPal may override, but overlay removal helps)
document.querySelectorAll(
'[id*="paypal"], [class*="paypal"], [data-paypal], [data-pp], ' +
'iframe[src*="paypal.com"], div[role="button"][aria-label*="PayPal"]'
).forEach(el => {
el.style.pointerEvents = 'auto';
el.style.opacity = '1';
el.style.cursor = 'pointer';
});
}
function checkAndProtect() {
const checkbox = document.getElementById(CHECKBOX_ID);
if (!checkbox) return;
const container = document.getElementById(CHECKBOX_CONTAINER_ID);
if (container) container.style.display = 'block';
// Disable until checked
if (!checkbox.checked) {
disablePayPalButtons();
}
checkbox.addEventListener('change', () => {
if (checkbox.checked) {
enablePayPalButtons();
console.log('Dapper: Checkbox checked → PayPal unlocked');
} else {
disablePayPalButtons();
console.log('Dapper: Checkbox unchecked → PayPal locked');
}
});
}
// Run immediately + watch for PayPal injection
checkAndProtect();
observer = new MutationObserver((mutations) => {
if (document.querySelector('[id*="paypal"], [class*="paypal"], iframe[src*="paypal.com"]')) {
checkAndProtect();
}
});
observer.observe(document.body, { childList: true, subtree: true });
// Retry safety net (Flatsome/PayPal can be very late)
setTimeout(checkAndProtect, 1500);
setTimeout(checkAndProtect, 4000);
setTimeout(checkAndProtect, 8000);
})();
</script>
<?php
}
// 2. Checkbox on CART page (near PayPal button)
add_action('woocommerce_proceed_to_checkout', 'dapper_human_checkbox_cart', 15);
function dapper_human_checkbox_cart() {
dapper_debug_log('Attempting cart checkbox output');
if (get_option('dapper_enable_paypal_human_check', 'on') !== 'on') {
return;
}
?>
<div id="dapper-human-check-cart" style="margin:20px 0; padding:15px; background:#fff3cd; border:1px solid #ffeeba; border-radius:4px; font-weight:bold;">
<strong>Human Verification Required for PayPal Express</strong><br>
<label style="display:block; margin:12px 0; font-size:1.1em;">
<input type="checkbox" id="dapper_human_checkbox_cart" required style="transform:scale(1.6); margin-right:12px; vertical-align:middle;">
Confirm I'm not a robot to unlock PayPal
</label>
<p style="color:#856404; font-size:0.95em;">This quick check helps stop automated spam. Takes 2 seconds.</p>
</div>
<script>
(function() {
console.log('Universal Dapper cart protection initialized');
const CHECKBOX_ID = 'dapper_human_checkbox_cart';
const CONTAINER_ID = 'dapper-human-check-cart';
function initCartCheckbox() {
const checkbox = document.getElementById(CHECKBOX_ID);
const container = document.getElementById(CONTAINER_ID);
if (!checkbox || !container) {
console.log('Dapper cart: Checkbox or container missing');
return false;
}
// Universal PayPal selectors (same as product page)
const paypalContainers = document.querySelectorAll(
'[id*="paypal"], [class*="paypal"], [data-paypal], [data-pp], ' +
'iframe[src*="paypal.com"], div[role="button"][aria-label*="PayPal"], ' +
'button[aria-label*="Pay with PayPal"], .pp-button, .smart-button, ' +
'.woocommerce-proceed-to-checkout .paypal-smart-button, ' +
'.wc-proceed-to-checkout .paypal-button-container'
);
if (paypalContainers.length === 0) {
console.log('Dapper cart: No PayPal containers found yet - retrying');
return false;
}
console.log('Dapper cart: Found ' + paypalContainers.length + ' PayPal container(s)');
// Force checkbox visible
container.style.display = 'block';
function disablePayPal() {
paypalContainers.forEach(container => {
container.style.pointerEvents = 'none';
container.style.opacity = '0.45';
container.style.cursor = 'not-allowed';
container.style.position = 'relative';
if (!container.querySelector('.dapper-universal-overlay')) {
const overlay = document.createElement('div');
overlay.className = 'dapper-universal-overlay';
overlay.style.position = 'absolute';
overlay.style.inset = '0';
overlay.style.background = 'rgba(0,0,0,0.3)';
overlay.style.display = 'flex';
overlay.style.alignItems = 'center';
overlay.style.justifyContent = 'center';
overlay.style.color = '#fff';
overlay.style.fontWeight = 'bold';
overlay.style.fontSize = '1.3em';
overlay.style.textAlign = 'center';
overlay.style.padding = '15px';
overlay.style.zIndex = '10';
overlay.innerText = 'Check verification box above to enable PayPal';
container.appendChild(overlay);
}
});
}
function enablePayPal() {
document.querySelectorAll('.dapper-universal-overlay').forEach(overlay => overlay.remove());
paypalContainers.forEach(container => {
container.style.pointerEvents = 'auto';
container.style.opacity = '1';
container.style.cursor = 'pointer';
});
}
// Start disabled
disablePayPal();
checkbox.addEventListener('change', () => {
if (checkbox.checked) {
enablePayPal();
console.log('Dapper cart: Checkbox checked → PayPal unlocked');
} else {
disablePayPal();
console.log('Dapper cart: Checkbox unchecked → PayPal locked');
}
});
return true;
}
// Run now + retry for dynamic load
let attempts = 0;
const maxAttempts = 20;
const interval = setInterval(() => {
if (initCartCheckbox() || attempts >= maxAttempts) {
clearInterval(interval);
}
attempts++;
}, 500);
initCartCheckbox();
})();
</script>
<?php
}
// 3. Server-side block: cancel any PayPal order missing valid token or showing bot patterns
add_action('woocommerce_new_order', 'dapper_block_fake_paypal_orders', 10, 1);
function dapper_block_fake_paypal_orders($order_id) {
$order = wc_get_order($order_id);
if (!$order) return;
$method = $order->get_payment_method();
if (!in_array($method, ['ppcp-gateway', 'paypal', 'ppec_paypal', 'paypal_express'], true)) {
return;
}
// Check token from product or cart (whichever was used)
$token = trim($_POST['dapper_human_token_product'] ?? $_POST['dapper_human_token_cart'] ?? '');
$time = (int) ($_POST['dapper_human_time_product'] ?? $_POST['dapper_human_time_cart'] ?? 0);
$token_ok = !empty($token) &&
strpos($token, 'human_') === 0 &&
strlen($token) >= 20 &&
(time() - $time <= 600); // 10 min window
// If token is missing/invalid → check other bot signs
if (!$token_ok) {
$first = strtolower(trim($order->get_billing_first_name() ?: ''));
$last = strtolower(trim($order->get_billing_last_name() ?: ''));
$company = strtolower(trim($order->get_billing_company() ?: ''));
$lowercase_name = ($first === $order->get_billing_first_name() && $last === $order->get_billing_last_name());
$suspicious_company = in_array($company, ['pogab', 'rayazifi', 'lunom', 'topap', 'potanginamo', '']) ||
preg_match('/^[a-z]{5,10}$/', $company);
if ($lowercase_name || $suspicious_company || (float)$order->get_total_tax() === 0) {
$order->update_status('cancelled', 'Blocked: Invalid or missing human verification for PayPal Express (bot pattern detected).');
dapper_debug_log("Blocked fake PayPal order #$order_id - No valid token | Name: $first $last | Company: $company");
}
}
}
// Trashes obvious Blocked paypal orders
add_action('woocommerce_order_status_cancelled', 'dapper_trash_obvious_bot_paypal_orders', 20, 1);
function dapper_trash_obvious_bot_paypal_orders($order_id) {
if (!class_exists('WooCommerce')) return;
$order = wc_get_order($order_id);
if (!$order) return;
// Only act on PayPal orders
$method = $order->get_payment_method();
if (!in_array($method, ['paypal', 'ppec_paypal', 'ppcp-gateway', 'paypal_express', 'braintree_paypal'], true)) {
return;
}
// Check if this cancellation came from our bot block function
$notes = $order->get_customer_order_notes();
$latest_note = !empty($notes) ? end($notes)->comment_content : '';
$bot_patterns = [
'Blocked: Invalid or missing human verification',
'bot pattern detected',
];
$is_bot = false;
foreach ($bot_patterns as $pattern) {
if (stripos($latest_note, $pattern) !== false) {
$is_bot = true;
break;
}
}
// Extra safety: very young cancelled orders with 0 tax and suspicious name/company
if (!$is_bot) {
$age_minutes = (time() - strtotime($order->get_date_created())) / 60;
$first = strtolower(trim($order->get_billing_first_name() ?: ''));
$last = strtolower(trim($order->get_billing_last_name() ?: ''));
$company = strtolower(trim($order->get_billing_company() ?: ''));
if ($age_minutes < 15 &&
(float)$order->get_total_tax() === 0 &&
($first === $last || preg_match('/^[a-z]{5,12}$/', $company))) {
$is_bot = true;
}
}
if ($is_bot) {
wp_trash_post($order_id);
$order->add_order_note('Moved to trash by Dapper — obvious PayPal bot order');
dapper_debug_log("Trashed obvious bot PayPal order #$order_id");
}
}
}
add_action( 'plugins_loaded', 'dapper_woo_conditional_load' ); // Late hook safe for class_exists()
// ────────────────────────────────────────────────
// Contact Form 7 protection (always loaded if CF7 exists)
// ────────────────────────────────────────────────
if (class_exists('WPCF7')) {
// Option to enable/disable CF7 protection separately
// You can add this to settings later if you want
add_action('wpcf7_before_send_mail', 'dapper_cf7_human_check', 9, 3);
function dapper_cf7_human_check($contact_form, &$abort, $submission) {
// Skip if disabled via future option
// if (get_option('dapper_enable_cf7_human_check', 'on') !== 'on') return;
$form_id = $contact_form->id();
// Very fast submission (< 5 seconds) → almost always bot
$posted_time = isset($_POST['dapper_cf7_time']) ? (int)$_POST['dapper_cf7_time'] : 0;
if ($posted_time && (time() - $posted_time < 5)) {
$abort = true;
$submission->add_error('dapper_speed', 'Submission too fast — please try again.');
dapper_debug_log("CF7 #$form_id blocked — too fast");
return;
}
// Honeypot field (random name)
foreach ($_POST as $key => $val) {
if (strpos($key, 'dapper_cf7_hp_') === 0 && strlen(trim($val)) > 0) {
$abort = true;
$submission->add_error('dapper_honeypot', 'Spam detected.');
dapper_debug_log("CF7 #$form_id blocked — honeypot filled");
return;
}
}
// Optional: require JS-set token (stronger)
$token = trim($_POST['dapper_cf7_token'] ?? '');
if (empty($token) || strpos($token, 'cf7human_') !== 0) {
$abort = true;
$submission->add_error('dapper_js', 'Please enable JavaScript and try again.');
dapper_debug_log("CF7 #$form_id blocked — missing/invalid JS token");
}
}
// Add fields + JS to every CF7 form
add_filter('wpcf7_form_elements', 'dapper_cf7_inject_anti_spam_fields');
function dapper_cf7_inject_anti_spam_fields($form) {
$honeypot_name = 'dapper_cf7_hp_' . wp_generate_password(7, false);
$hidden_fields = '
<input type="hidden" name="dapper_cf7_time" value="' . time() . '" />
<input type="hidden" name="dapper_cf7_token" id="dapper_cf7_token" value="" />
<p style="position:absolute; left:-9999px; height:1px; overflow:hidden;">
<input type="text" name="' . esc_attr($honeypot_name) . '" value="" autocomplete="off" tabindex="-1" />
</p>';
// Insert just before </form>
$form = str_replace('</form>', $hidden_fields . '</form>', $form);
return $form;
}
// Tiny JS — add to footer when CF7 shortcode exists on page
add_action('wp_footer', 'dapper_cf7_anti_spam_js', 90);
function dapper_cf7_anti_spam_js() {
if (!function_exists('wpcf7') || !did_action('wp_footer')) return;
?>
<script>
document.addEventListener('DOMContentLoaded', () => {
document.querySelectorAll('.wpcf7 form').forEach(form => {
const tokenField = form.querySelector('#dapper_cf7_token');
if (tokenField) {
tokenField.value = 'cf7human_' + Math.random().toString(36).substring(2) + '_' + Date.now();
}
});
});
</script>
<?php
}
add_filter( 'wpcf7_form_elements', 'dapper_cf7_inject_human_checkbox', 20 );
function dapper_cf7_inject_human_checkbox( $form ) {
if ( get_option( 'dapper_enable_cf7_human_checkbox', 'on' ) !== 'on' ) {
return $form;
}
$checkbox_html = '
<div class="dapper-cf7-human-check" style="margin: 1.5em 0; padding: 1em; background: #f8f9fa; border: 1px solid #ccd0d4; border-radius: 4px; text-align: center;">
<label style="font-size: 1.1em; cursor: pointer; user-select: none;">
<input type="checkbox" name="dapper_cf7_human_confirm" id="dapper_cf7_human_confirm" value="1" required style="transform: scale(1.4); margin-right: 0.8em; vertical-align: middle;">
I am human / not a robot
</label>
<input type="hidden" name="dapper_cf7_human_token" id="dapper_cf7_human_token" value="">
<input type="hidden" name="dapper_cf7_human_time" value="' . time() . '">
<p style="margin: 0.6em 0 0; font-size: 0.9em; color: #555;">Quick check to help stop spam. Thanks!</p>
</div>';
// Insert just before the submit button / </form>
$form = preg_replace( '/(<button[^>]*type=["\']submit["\'][^>]*>.*?<\/button>)/is', $checkbox_html . '$1', $form );
// Fallback: if no <button type="submit"> found, put before </form>
if ( strpos( $form, $checkbox_html ) === false ) {
$form = str_replace( '</form>', $checkbox_html . '</form>', $form );
}
return $form;
}
// 2. Very small JS — runs on every page that has CF7 (cheap)
add_action( 'wp_footer', 'dapper_cf7_human_checkbox_js', 95 );
function dapper_cf7_human_checkbox_js() {
// Only output if at least one CF7 form exists on page
if ( ! did_action( 'wpcf7_enqueue_scripts' ) ) {
return;
}
?>
<script>
document.addEventListener('DOMContentLoaded', () => {
document.querySelectorAll('.wpcf7 form').forEach(form => {
const checkbox = form.querySelector('#dapper_cf7_human_confirm');
const tokenField = form.querySelector('#dapper_cf7_human_token');
if (!checkbox || !tokenField) return;
// Enable token only when checked
checkbox.addEventListener('change', () => {
tokenField.value = checkbox.checked
? 'cf7_human_' + Math.random().toString(36).substring(2,10) + '_' + Date.now()
: '';
});
// Optional: disable submit until checked (stronger UX)
const submitBtn = form.querySelector('input[type="submit"], button[type="submit"]');
if (submitBtn) {
submitBtn.disabled = true;
checkbox.addEventListener('change', () => {
submitBtn.disabled = !checkbox.checked;
});
}
});
});
</script>
<?php
}
// 3. Server-side validation — block if missing / invalid / too fast
add_action( 'wpcf7_before_send_mail', 'dapper_cf7_validate_human_checkbox', 11, 3 );
function dapper_cf7_validate_human_checkbox( $contact_form, &$abort, $submission ) {
if ( get_option( 'dapper_enable_cf7_human_checkbox', 'on' ) !== 'on' ) {
return;
}
$posted = $_POST;
// A. Checkbox must be checked
if ( empty( $posted['dapper_cf7_human_confirm'] ) ) {
$abort = true;
$submission->add_error( 'dapper_human', __( 'Please confirm you are human.', 'dapper' ) );
dapper_debug_log( "CF7 #{$contact_form->id()} blocked — human checkbox not checked" );
return;
}
// B. Token must exist and look valid
$token = trim( $posted['dapper_cf7_human_token'] ?? '' );
if ( empty( $token ) || strpos( $token, 'cf7_human_' ) !== 0 || strlen( $token ) < 18 ) {
$abort = true;
$submission->add_error( 'dapper_human', __( 'Verification failed. Please try again.', 'dapper' ) );
dapper_debug_log( "CF7 #{$contact_form->id()} blocked — invalid/missing human token" );
return;
}
// C. Not submitted in < 4 seconds (very strong signal of bot)
$time = (int) ( $posted['dapper_cf7_human_time'] ?? 0 );
if ( $time && ( time() - $time < 4 ) ) {
$abort = true;
$submission->add_error( 'dapper_human', __( 'Submission too fast — please try again.', 'dapper' ) );
dapper_debug_log( "CF7 #{$contact_form->id()} blocked — human check too fast" );
}
}
}
function add_campaign_management_page() {
add_submenu_page(
'edit.php?post_type=email_template', // Parent menu slug
'Campaigns', // Page title
'Campaigns', // Menu title
'manage_options', // Capability required
'email-campaigns', // Page slug
'render_campaign_management_page' // Callback function to render the page
);
}
add_action('admin_menu', 'add_campaign_management_page');
function render_campaign_management_page() {
?>
<div class="wrap">
<h2>Email Campaigns</h2>
<ul>
<?php
$campaigns = get_posts(array('post_type' => 'email_campaign', 'posts_per_page' => -1));
foreach ($campaigns as $campaign) {
echo '<li>
<a href="' . get_edit_post_link($campaign->ID) . '">' . esc_html($campaign->post_title) . '</a>
- <a href="' . get_delete_post_link($campaign->ID) . '">Delete</a>
</li>';
}
?>
</ul>
<a href="<?php echo admin_url('post-new.php?post_type=email_campaign'); ?>" class="button">Add New Campaign</a>
</div>
<?php
}
/*
// Updates a users subscription status
// Called when only when changing a status
// @param $user_id integer id of passed in users
// @param $new_status string "subscribed" or "unsubscribed"
*/
function update_subscription_status($user_id, $new_status) {
global $wpdb;
$table_name = $wpdb->prefix . 'dapper_mailing_list';
// Update the subscription status
$wpdb->update(
$table_name,
array('subscription_status' => $new_status),
array('user_id' => $user_id)
);
}
/*
// Displays the Mailing List users on the setting page
// Only one call
*/
function display_subscribed_users() {
if ( ! current_user_can('manage_options') ) {
wp_die( __('Sorry, you are not allowed to manage this.', 'dapper') );
}
// ------------------- ADD THIS SECURITY CHECK -------------------
// Only process if it's a POST from our form
if ( $_SERVER['REQUEST_METHOD'] === 'POST' ) {
// This checks the nonce AND dies with error if invalid/missing
check_admin_referer( 'dapper_subscription_action', 'dapper_subscription_nonce' );
// Now it's safe to process
if (isset($_POST['update_subscription_status'])) {
$user_id = intval($_POST['user_id']);
$new_status = sanitize_text_field($_POST['new_status']);
update_subscription_status($user_id, $new_status);
}
if (isset($_POST['remove_user_from_list'])) {
$user_id = intval($_POST['user_id']);
remove_user_from_mailing_list($user_id);
}
}
// Retrieve mailing list users
$mailing_list_users = get_mailing_list_users();
// Display users in HTML
echo '<h2>Mailing List Users</h2>';
echo '<table>';
echo '<tr><th>Email</th><th>Status</th><th>Action</th></tr>';
foreach ($mailing_list_users as $user) {
echo '<tr>';
echo '<td>' . esc_html($user->email) . '</td>';
echo '<td>' . esc_html($user->subscription_status) . '</td>';
echo '<td>';
echo '<form method="post">';
echo wp_nonce_field( 'dapper_subscription_action', 'dapper_subscription_nonce');
echo '<input type="hidden" name="user_id" value="' . esc_attr($user->user_id) . '">';
echo '<select name="new_status">';
echo '<option value="subscribed">Subscribed</option>';
echo '<option value="unsubscribed">Unsubscribed</option>';
echo '</select>';
echo '<input type="submit" name="update_subscription_status" value="Update Status">';
echo '<input type="submit" name="remove_user_from_list" value="Remove">';
echo '</form>';
echo '</td>';
echo '</tr>';
}
echo '</table>';
}
function dapper_settings_page() {
add_submenu_page(
'edit.php?post_type=email_template',
'Dapper Settings',
'Settings',
'manage_options',
'dapper-settings',
'dapper_settings_page_content'
);
}
function dapper_settings_page_content() {
dapper_debug_log('settings page');
?>
<div class="wrap">
<h2>Dapper Settings</h2>
<h2 class= "nav-tab-wrapper">
<a href="#general" class="nav-tab">General</a>
<a href="#email-settings" class="nav-tab">Email Settings</a>
<a href="#subscription" class="nav-tab">Mailing List</a>
<a href="#backup" class="nav-tab">Backups</a>
</h2>
<div id="general">
<form method="post" action="options.php">
<?php
settings_fields('dapper-settings-group');
do_settings_sections('dapper-settings');
?>
<h3>Debug Mode</h3>
<p>Debug Mode adds entries to the webserver error log file.</p>
<label for="dapper_enable_debug">Enable Debug Mode</label>
<input type="checkbox" id="dapper_enable_debug" name="dapper_enable_debug"<?php if (get_option('dapper_enable_debug') == 'on') echo "checked='checked'"; ?>>
<h3>Custom Admin Panel Login Logo</h5>
<p>Toggles to enable the custom logo on the admin panel login.</p>
<label for="dapper_admin_panel_custom_logo_enable">Enable Custom Logo</label>
<input type="checkbox" id="dapper_admin_panel_custom_logo_enable" name="dapper_admin_panel_custom_logo_enable"<?php if (get_option('dapper_admin_panel_custom_logo_enable') == 'on') echo "checked='checked'"; ?>>
<p>Image files need to be formated for 120 pixels x 120 pixels
</p>
<label for="dapper_admin_panel_custom_logo_path">Logo Path:</label>
<input type="text" id="dapper_admin_panel_custom_logo_path" name="dapper_admin_panel_custom_logo_path" value="<?php echo esc_attr(get_option('dapper_admin_panel_custom_logo_path')); ?>">
<input type="button" value="Upload Image" class="button-secondary" id="upload_image_button" />
<p>I Love Lana</p>
<div id="custom_logo_preview">
<?php
$custom_logo_url = esc_url(get_option('dapper_admin_panel_custom_logo_path'));
if ($custom_logo_url) {
echo '<img src="' . $custom_logo_url . '" alt="Custom Logo Preview" style="max-width: 120px; max-height: 120px;" />';
} else {
// Show a placeholder image when the custom logo is not set
echo '<img src="' . content_url() . '/uploads/woocommerce-placeholder-247x296.png" alt="Generic Image" style="max-width: 120px; max-height: 120px;" />';
}
?>
</div>
<h3>WooCommerce Integration</h3>
<p>Enable automatic order status emails, tracking notifications, and registration anti-spam honeypot. Disable if not using WooCommerce.</p>
<label for="dapper_enable_woo_integration">Enable WooCommerce Features</label>
<input type="checkbox" id="dapper_enable_woo_integration" name="dapper_enable_woo_integration" <?php checked( get_option( 'dapper_enable_woo_integration', 'on' ), 'on' ); ?>>
<h3>Paypal Human Verification Checkbox</h3>
<?php
$woo_enabled = get_option('dapper_enable_woo_integration', 'on') === 'on';
$paypal_forced_off = !$woo_enabled;
?>
<label for="dapper_enable_paypal_human_check">
Enable Human Verification for PayPal/Add to Cart
<?php if ($paypal_forced_off): ?>
<span style="color:#d63638; font-size:0.95em;">(requires WooCommerce integration)</span>
<?php endif; ?>
</label>
<input type="checkbox" id="dapper_enable_paypal_human_check" name="dapper_enable_paypal_human_check"
<?php checked(get_option('dapper_enable_paypal_human_check', 'on'), 'on'); ?>
<?php echo $paypal_forced_off ? 'disabled' : ''; ?>>
<?php
<h3>Contact Form 7 Protection</h3>
<label for="dapper_enable_cf7_human_checkbox">Enable "I'm human" checkbox on all CF7 forms</label>
<input type="checkbox" id="dapper_enable_cf7_human_checkbox" name="dapper_enable_cf7_human_checkbox"
<?php checked( get_option( 'dapper_enable_cf7_human_checkbox', 'on' ), 'on' ); ?>
submit_button();
?>
</form>
<div class="notice notice-info inline" style="margin: 40px 0 20px; padding: 12px 16px; text-align: center; border-left: 4px solid #2271b1;">
<p style="margin: 0; font-size: 13px; color: #1d2327;">
<strong>Dapper v<?php echo esc_html( defined('DAPPER_VERSION') ? DAPPER_VERSION : 'unknown' ); ?></strong>
• commit
<code style="background: #f0f0f1; padding: 2px 6px; border-radius: 3px; font-family: Consolas, Monaco, 'Lucida Console', monospace;">
<?php echo esc_html( DAPPER_BUILD ); ?>
</code>
<?php if ( DAPPER_BUILD === 'dev-local' ) : ?>
<span style="color: #d63638; font-style: italic;">(local dev no git detected)</span>
<?php endif; ?>
</p>
</div>
</div>
<div id="email-settings">
<form method="post" action="options.php">
<?php
settings_fields('dapper-email-settings-group');
do_settings_sections('dapper-settings');
?>
<h3>Email Header</h3>
<p>Example: From: Your Name &lt;youremail@example.com&gt; <br>Content-Type: text/html Reply-to: Your Name</p>
<label for="dapper_headers">Email Headers:</label>
<input type="text" id="dapper_headers" name="dapper_headers" value="<?php echo esc_attr(get_option('dapper_headers')); ?>">
<h3>Review Link Structure</h3>
<p>Example: https://www.example.com/products</p>
<label for="dapper_review_link_structure">Review Link Structure:</label>
<input type="text" id="dapper_review_link_structure" name="dapper_review_link_structure" value="<?php echo esc_attr(get_option('dapper_review_link_structure')); ?>"><label for="dapper_review_link_structure_end">/$product_slug/#reviews</label>
<h3>Order Status Delivered</h3>
<p>Enter your template for WooCommerce Order Status Delivered Leave blank to disable</p>
<label for="dapper_woo_status_delivered">WooCommerce Order Status Delivered:</label>
<input type="text" id="dapper_woo_status_delivered" name="dapper_woo_status_delivered" value="<?php echo esc_attr(get_option('dapper_woo_status_delivered')); ?>">
<h3>Order Status Shipped</h3>
<p>Enter your template for WooCommerce Order Status Shipped Leave blank to disable</p>
<label for="dapper_woo_status_shipped">WooCommerce Order Status Shipped:</label>
<input type="text" id="dapper_woo_status_shipped" name="dapper_woo_status_shipped" value="<?php echo esc_attr(get_option('dapper_woo_status_shipped')); ?>">
<h3>Order Status Partially Shipped</h3>
<p>Enter your template for WooCommerce Order Status Partially Shipped Leave blank to disable</p>
<label for="dapper_woo_status_partial_shipped">WooCommerce Order Status Partially Shipped:</label>
<input type="text" id="dapper_woo_status_partial_shipped" name="dapper_woo_status_partial_shipped" value="<?php echo esc_attr(get_option('dapper_woo_status_partial_shipped')); ?>">
<h3>Order Status Pending Payment</h3>
<p>Enter your template for WooCommerce Order Status Pending Payment Leave blank to disable</p>
<label for="dapper_woo_status_pending_payment">WooCommerce Order Status pending Payment:</label>
<input type="text" id="dapper_woo_status_pending_payment" name="dapper_woo_status_pending_payment" value="<?php echo esc_attr(get_option('dapper_woo_status_pending_payment')); ?>">
<h3>Order Status Processing</h3>
<p>Enter your template for WooCommerce Order Status Processing Leave blank to disable</p>
<label for="dapper_woo_status_processing">WooCommerce Order Status Processing:</label>
<input type="text" id="dapper_woo_status_processing" name="dapper_woo_status_processing" value="<?php echo esc_attr(get_option('dapper_woo_status_processing')); ?>">
<h3>Order Status Failed</h3>
<p>Enter your template for WooCommerce Order Status Failed Leave blank to disable</p>
<label for="dapper_woo_status_failed">WooCommerce Order Status Failed:</label>
<input type="text" id="dapper_woo_status_failed" name="dapper_woo_status_failed" value="<?php echo esc_attr(get_option('dapper_woo_status_failed')); ?>">
<h3>Order Status Draft</h3>
<p>Enter your template for WooCommerce Order Status Draft Leave blank to disable</p>
<label for="dapper_woo_status_draft">WooCommerce Order Status Draft:</label>
<input type="text" id="dapper_woo_status_draft" name="dapper_woo_status_draft" value="<?php echo esc_attr(get_option('dapper_woo_status_draft')); ?>">
<h3>Order Status Refunded</h3>
<p>Enter your template for WooCommerce Order Status Refunded Leave blank to disable</p>
<label for="dapper_woo_status_refunded">WooCommerce Order Status Refunded:</label>
<input type="text" id="dapper_woo_status_refunded" name="dapper_woo_status_refunded" value="<?php echo esc_attr(get_option('dapper_woo_status_refunded')); ?>">
<h3>Order Status Cancelled</h3>
<p>Enter your template for WooCommerce Order Status Cancelled Leave blank to disable</p>
<label for="dapper_woo_status_cancelled">WooCommerce Order Status Cancelled:</label>
<input type="text" id="dapper_woo_status_cancelled" name="dapper_woo_status_cancelled" value="<?php echo esc_attr(get_option('dapper_woo_status_cancelled')); ?>">
<h3>Order Status On Hold</h3>
<p>Enter your template for WooCommerce Order Status On Hold Leave blank to disable</p>
<label for="dapper_woo_status_on_hold">WooCommerce Order Status On Hold:</label>
<input type="text" id="dapper_woo_status_on_hold" name="dapper_woo_status_on_hold" value="<?php echo esc_attr(get_option('dapper_woo_status_on_hold')); ?>">
<?php
submit_button();
?>
</form>
</div>
<div id="subscription">
<?php display_subscribed_users(); ?>
</div>
<div id="backup">
<?php
if (isset($_GET['backup'])) {
if ($_GET['backup'] === 'success') {
echo '<div class="notice notice-success is-dismissible"><p>Backup created successfully! Refresh to see new files below.</p></div>';
} elseif ($_GET['backup'] === 'failed') {
echo '<div class="notice notice-error is-dismissible"><p>Backup failed — check debug log. Try unchecking some folders if memory is low.</p></div>';
}
}
?>
<form method="post" action="options.php">
<?php settings_fields('dapper-backup-group'); ?>
<h3>Backup Settings</h3>
<p>Configure what to include in backups. Uncheck large folders if you have memory limits.</p>
<label><input type="checkbox" name="dapper_backup_themes" <?php checked(get_option('dapper_backup_themes', 'on'), 'on'); ?>> Include Themes folder</label><br>
<label><input type="checkbox" name="dapper_backup_plugins" <?php checked(get_option('dapper_backup_plugins', 'on'), 'on'); ?>> Include Plugins folder</label><br>
<label><input type="checkbox" name="dapper_backup_include_media" <?php checked(get_option('dapper_backup_include_media'), 'on'); ?>> Include Media (/uploads) — <strong>Warning: may exceed 128 MB limit on large sites</strong></label>
<?php submit_button('Save Backup Settings'); ?>
</form>
<!-- Manual Backup -->
<form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>" style="margin: 30px 0;">
<input type="hidden" name="action" value="dapper_manual_backup">
<?php wp_nonce_field('dapper_create_backup_nonce'); ?>
<input type="submit" name="dapper_create_backup_now" class="button button-primary button-large" value="Create Backup Now">
<p class="description">Creates database export + selected folders. May take time on large sites.</p>
</form>
<?php
// Safe call — only if function exists
if (function_exists('display_old_backup_list')) {
display_old_backup_list();
} else {
echo '<p style="color:red;">Backup list function not available. Check if dapper_backup.php loaded correctly (syntax error?).</p>';
}
?>
</div>
</div>
<script>
jQuery(function($){
$('.nav-tab-wrapper a').click(function(){
var tab_id = $(this).attr('href');
$('.nav-tab-wrapper a').removeClass('nav-tab-active');
$('.wrap > div').hide();
$(this).addClass('nav-tab-active');
$(tab_id).show();
return false;
});
// Hide all content divs except the one initially active
$('.wrap > div').not(':first').hide();
});
</script>
<?php
}
add_action('admin_menu', 'dapper_settings_page');
/*
// Load media uploader and custom admin JS only on Dapper settings page
*/
function enqueue_media_uploader_scripts() {
// Check if we're on the Dapper settings page
if ( isset( $_GET['page'] ) && $_GET['page'] === 'dapper-settings' ) {
// Load WordPress media library (for image upload button)
wp_enqueue_media();
// Load our admin.js (handles upload button + preview)
wp_enqueue_script(
'my-admin-js',
plugin_dir_url( __FILE__ ) . 'admin.js',
array( 'jquery' ),
null,
false
);
}
}
add_action( 'admin_enqueue_scripts', 'enqueue_media_uploader_scripts' );
/*
// Retrieves the mailing list user with search and pagination
// @param $search string passed in from search bar
// @param $page integer used as a page placeholder
// @param $per_page integer holds the number of users per page
// Returns user search results
*/
function getting_mailing_list_users($search, $page, $per_page) {
global $wpdb;
$offset = ($page - 1) * $per_page;
$table_name = $wpdb->prefix . 'dapper_mailing_list';
$where = '';
if (!empty($search)) {
$where = "WHERE email LIKE '%$search%'";
}
$sql = "SELECT * FROM $table_name $where LIMIT $per_page OFFSET $offset";
$users = $wpdb->get_results($sql);
return $users;
}
/*
// Register plugin settings through wordpress method register_setting
*/
function dapper_register_settings() {
register_setting('dapper-settings-group', 'dapper_option');
register_setting('dapper-email-settings-group', 'dapper_headers');
register_setting('dapper-email-settings-group', 'dapper_review_link_structure');
register_setting('dapper-email-settings-group', 'dapper_woo_status_delivered');
register_setting('dapper-email-settings-group', 'dapper_woo_status_shipped');
register_setting('dapper-email-settings-group', 'dapper_woo_status_partial_shipped');
register_setting('dapper-email-settings-group', 'dapper_woo_status_processing');
register_setting('dapper-email-settings-group', 'dapper_woo_status_failed');
register_setting('dapper-email-settings-group', 'dapper_woo_status_draft');
register_setting('dapper-email-settings-group', 'dapper_woo_status_refunded');
register_setting('dapper-email-settings-group', 'dapper_woo_status_cancelled');
register_setting('dapper-email-settings-group', 'dapper_woo_status_on_hold');
register_setting('dapper-email-settings-group', 'dapper_woo_status_pending_payment');
register_setting('dapper-settings-group', 'dapper_enable_debug');
register_setting('dapper-settings-group', 'dapper_admin_panel_custom_logo_path');
register_setting('dapper-settings-group', 'dapper_admin_panel_custom_logo_enable');
register_setting('dapper-settings-group', 'dapper_setup_complete');
register_setting('dapper-settings-group', 'dapper_enable_backup');
register_setting('dapper-backup-group', 'dapper_enable_backup');
register_setting('dapper-settings-group', 'dapper_enable_woo_integration');
register_setting('dapper-backup-group', 'dapper_backup_include_media');
register_setting('dapper-backup-group', 'dapper_backup_themes');
register_setting('dapper-backup-group', 'dapper_backup_plugins');
register_setting('dapper-backup-group', 'dapper_backup_include_media');
register_setting('dapper-settings-group', 'dapper_enable_paypal_human_check');
register_setting( 'dapper-settings-group', 'dapper_enable_cf7_human_checkbox' );
// Add other settings as needed
}
add_action('admin_init', 'dapper_register_settings');
/*
// Debug wrapper to add Dapper Debug to the front of all debuging messages
// Call this instead of directly calling error_log
// @param $message string of passed in message required
*/
function dapper_debug_log($message) {
if (get_option('dapper_enable_debug', 0) == 'on') {
error_log('[Dapper Debug] ' . $message);
}
}
/*
// Utility function used to get contents of an email template
// to then swap out place holder data with real data
// @param $template_name string name of passed in email template required
// Returns an empty string if the passed in email template doesn't exist
*/
function get_email_template_content_by_name($template_name) {
$template_post = get_page_by_title($template_name, OBJECT, 'email_template');
if ($template_post) {
// Use 'post_content' to get the HTML content of the post
return $template_post->post_content;
}
dapper_debug_log( "No email template found with title: " . $template_name );
return ''; // Return an empty string if the template is not found
}
add_action('updated_option', 'dapper_sync_paypal_human_check_with_woo', 10, 3);
/*
// Automatically disable PayPal Human Verification when WooCommerce integration is turned off.
//
// This prevents the human verification checkbox from staying enabled when
// WooCommerce features are disabled (since the checkbox relies on WooCommerce hooks).
//
// Hook: updated_option
// @param string $option The updated option name.
// @param mixed $old_value Previous value.
// @param mixed $value New value.
//
// @return void
*/
function dapper_sync_paypal_human_check_with_woo( $option, $old_value, $value ) {
// Only care about the WooCommerce integration toggle
if ( 'dapper_enable_woo_integration' !== $option ) {
return;
}
// If WooCommerce integration is being turned off
if ( 'on' !== $value ) {
// Force-disable the PayPal human verification checkbox feature
update_option( 'dapper_enable_paypal_human_check', 'off' );
// Debug trace when enabled
if ( get_option( 'dapper_enable_debug', 'off' ) === 'on' ) {
dapper_debug_log(
sprintf(
'WooCommerce integration disabled → PayPal Human Verification turned off (old: %s → new: %s)',
esc_html( $old_value ?? 'unset' ),
esc_html( $value )
)
);
}
}
}
/*
// Retrieves the order total in Numeric value only
// @param $order_id integer of passed in order id required
// Returns 0 if $order_id is null
*/
function get_order_total_numeric($order_id) {
if (! class_exists('WooCommerce')){
return 0;
}
$order = wc_get_order($order_id);
return $order ? (float) $order->get_total() : 0;
}
/*
// Persistent Object Cache clearing on update to prevent a race condition
// @param $option passed in string from hook
*/
function dapper_clear_alloptions_cache($option) {
if ( ! wp_installing() ) {
$alloptions = wp_load_alloptions();
if ( isset( $alloptions[ $option ] ) ) {
wp_cache_delete( 'alloptions', 'options' );
}
}
}
add_action( 'added_option', 'dapper_clear_alloptions_cache' );
add_action( 'updated_option', 'dapper_clear_alloptions_cache' );
add_action( 'deleted_option', 'dapper_clear_alloptions_cache' );
/*
// Customizing the login form on the admin panel
// place your custom logo in your active theme folder inside a folder
// named "images" and on the Dapper pluging settings page enter
// the logo image path
*/
function dapper_login_logo() {
if (get_option('dapper_admin_panel_custom_logo_enable') === 'on') {
?>
<style type="text/css">
#login h1 a, .login h1 a {
background-image: url(<?php echo esc_url( get_option('dapper_admin_panel_custom_logo_path') ); ?>);
height:120px;
width:120px;
background-size: 120px 120px;
background-repeat: no-repeat;
padding-bottom: 30px;
}
</style>
<?php }
}
add_action( 'login_enqueue_scripts', 'dapper_login_logo' );
/**
* Load plugin text domain for translations.
*/
function dapper_load_textdomain() {
load_plugin_textdomain( 'dapper', false, dirname( DAPPER_BASENAME ) . '/languages' );
}
add_action( 'init', 'dapper_load_textdomain' );
// Handle manual backup via admin-post (secure & reliable)
add_action('admin_post_dapper_manual_backup', 'dapper_process_manual_backup');
function dapper_process_manual_backup() {
set_time_limit(0);
if (!current_user_can('manage_options')) {
wp_die('Access denied.');
}
check_admin_referer('dapper_create_backup_nonce');
set_transient('dapper_backup_in_progress', true, 600); // 10 min lock
$result = dapper_create_new_backup();
delete_transient('dapper_backup_in_progress');
if ($result !== false) {
$redirect = add_query_arg(
array(
'page' => 'dapper-settings',
'backup' => 'success',
),
admin_url('admin.php')
);
} else {
$redirect = add_query_arg(
array(
'page' => 'dapper-settings',
'backup' => 'failed',
),
admin_url('admin.php')
);
}
wp_safe_redirect($redirect);
exit;
}