From 17d8257524c89d3be0b62b8854cf017e287a401c Mon Sep 17 00:00:00 2001 From: carpentryplus25 Date: Wed, 25 Feb 2026 13:02:00 -0500 Subject: [PATCH] Initial commit --- .DS_Store | Bin 0 -> 6148 bytes .gitattributes | 2 + CHANGELOG.md | 40 ++ admin.js | 32 ++ dapper.php | 1259 +++++++++++++++++++++++++++++++++++++++++++++ dapper_backup.php | 402 +++++++++++++++ readme.txt | 75 +++ 7 files changed, 1810 insertions(+) create mode 100644 .DS_Store create mode 100644 .gitattributes create mode 100644 CHANGELOG.md create mode 100644 admin.js create mode 100644 dapper.php create mode 100644 dapper_backup.php create mode 100644 readme.txt diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..62ab4d472933a7c8e43c61dfd423b018a08a3ae5 GIT binary patch literal 6148 zcmeHLu};G<5Iv_ADrG4fW4-{DD6+K^PMcRrs z#~~^pn>(X9<#bQA{Y$bU!GiH%1Q*!e;g=eGLqyN0#P5!tu;V2yspLK1aojsNL5*|T z;M`Dx3@JU(8tm>26J+SsG8|!jIamwK>YuRPY_!3uHWKSaZp3Db@iZ;+(vL{h7|)A1 z)?9;+=;dwo`gw`4y_os0G3Bx~Pi;zTngXVPDR4jv;LK(loCsQN3YY?>z*Yf%KZGcZ zkzyt2Umd9Y6#&?W+Z)<^mtan)7%5hQ7=byV0)^`Iis6I~f2eVhVkIbad|di2Ii1<* z4aND{kw4V&NYH9iz!YdI(39?n&;P5R@BdAb^-KX%;9n`=`tdZz-z_+FyN%D<5akwy ojpHi8Ed>pK6eE_8;xiO)@Q2(0Mv9dnJTUtZ@G@9s3jC=8-(C7?_W%F@ literal 0 HcmV?d00001 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..dfe0770 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Auto detect text files and perform LF normalization +* text=auto diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..8398657 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,40 @@ +# Dapper Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [1.1.1] - 2026-02-23 + +### Added +- Plugin URI (https://theblueduckpond.com) and Author URI (https://eastofthehaw.com) in header +- WooCommerce checkout anti-spam layers: honeypot fields, JavaScript validation field, submission timestamp check (blocks fast/automated bots) +- Basic backup functionality in settings → Backups tab: + - Manual "Create Backup Now" button + - Pure-PHP database export (.sql) and essential files zip (themes/plugins/uploads) + - Secure, nonce-protected download links for backups + - Simple retention (keeps last 7 backups) + - Protected backup directory (/wp-content/dapper-backups) + +### Changed +- Bumped version to 1.1.1 +- Improved plugin header description for better clarity and SEO +- Formal changelog implementation (this file for full details + short version in readme.txt) +- Minor UI and settings page cleanups (backup tab integration) + +### Fixed +- Various code organization, comment consistency, and potential edge-case improvements + +### Notes +- Pre-1.1.1 changes were not formally tracked (many incremental improvements to email handling, templates, mailing list, and custom login logo over time) + +### Planned / In Development +### Planned Additions +- Scheduled (cron-based) automated backups +- Backup retention policies and configurable limits +- Simple one-click restore functionality +- Email notifications on backup success/failure +- Exclusion filters for large folders/files in zip + +... diff --git a/admin.js b/admin.js new file mode 100644 index 0000000..010ab64 --- /dev/null +++ b/admin.js @@ -0,0 +1,32 @@ +jQuery(document).ready(function ($) { + $('#upload_image_button').click(function (e) { + e.preventDefault(); + var custom_logo_input = $('#dapper_admin_panel_custom_logo_path'); + var custom_logo_url = custom_logo_input.val(); + var custom_logo_preview = $('#custom_logo_preview'); + + var image = wp.media({ + title: 'Upload Custom Logo', + multiple: false, + }).open() + .on('select', function (e) { + var uploaded_image = image.state().get('selection').first(); + var image_url = uploaded_image.toJSON().url; + custom_logo_input.val(image_url); + + // Display the preview dynamically + custom_logo_preview.html('Custom Logo Preview'); + }); + + // If a custom logo URL is already set, pre-select it in the media uploader + if (custom_logo_url !== '') { + var selection = image.state().get('selection'); + var attachment = wp.media.attachment(custom_logo_url); + attachment.fetch(); + selection.add(attachment ? [attachment] : []); + + // Display the preview for the initially set logo + custom_logo_preview.html('Custom Logo Preview'); + } + }); +}); diff --git a/dapper.php b/dapper.php new file mode 100644 index 0000000..ebd1cab --- /dev/null +++ b/dapper.php @@ -0,0 +1,1259 @@ +ID, $user->user_email, 'subscribed'); + } + make_images_dir(); + make_backup_dir(); + // Mark setup completed + update_option('dapper_setup_complete', true); + } +} +register_activation_hook(__FILE__, 'dapper_activation'); + +/* +// 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 + ?> + +

+ +

+ $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; + } + // =================== + // 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 + ?> +
+ + + +
+ + + + $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 + + + + + add('human_check_error', __('Please verify you are human to use PayPal.', 'dapper')); + return; + } + + // Must be checked within 5 minutes + if (time() - $time > 300) { + $errors->add('human_check_error', __('Verification timed out. Please try again.', 'dapper')); + return; + } + + // Optional: token must start with 'human_' and be random-ish + if (strpos($token, 'human_') !== 0 || strlen($token) < 20) { + $errors->add('human_check_error', __('Invalid verification. Please try again.', 'dapper')); + } + } +} + + + +} +add_action( 'plugins_loaded', 'dapper_woo_conditional_load' ); // Late hook – safe for class_exists() + + + + +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() { + ?> +
+

Email Campaigns

+ + + Add New Campaign + + +
+ 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 '

Mailing List Users

'; + echo ''; + echo ''; + foreach ($mailing_list_users as $user) { + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + } + echo '
EmailStatusAction
' . esc_html($user->email) . '' . esc_html($user->subscription_status) . ''; + echo '
'; + echo wp_nonce_field( 'dapper_subscription_action', 'dapper_subscription_nonce'); + echo ''; + echo ''; + echo ''; + echo ''; + echo '
'; + echo '
'; +} + + +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'); +?> +
+

Dapper Settings

+ + +
+
+ +

Debug Mode

+

Debug Mode adds entries to the webserver error log file.

+ + > + + > +

Shows a quick "I'm not a robot" checkbox when PayPal is selected. Helps block bots without third-party services.

+

Custom Admin Panel Login Logo

+

Toggles to enable the custom logo on the admin panel login.

+ + > +

Image files need to be formated for 120 pixels x 120 pixels +

+ + + +

I Love Lana

+
+ '; + } else { + // Show a placeholder image when the custom logo is not set + echo 'Generic Image'; + } + ?> +
+

WooCommerce Integration

+

Enable automatic order status emails, tracking notifications, and registration anti-spam honeypot. Disable if not using WooCommerce.

+ + > + + + +
+ +
+
+
+ + +

Email Header

+

Example: From: Your Name <youremail@example.com>
Content-Type: text/html Reply-to: Your Name

+ + +

Review Link Structure

+

Example: https://www.example.com/products

+ + +

Order Status Delivered

+

Enter your template for WooCommerce Order Status Delivered Leave blank to disable

+ + +

Order Status Shipped

+

Enter your template for WooCommerce Order Status Shipped Leave blank to disable

+ + +

Order Status Partially Shipped

+

Enter your template for WooCommerce Order Status Partially Shipped Leave blank to disable

+ + +

Order Status Pending Payment

+

Enter your template for WooCommerce Order Status Pending Payment Leave blank to disable

+ + +

Order Status Processing

+

Enter your template for WooCommerce Order Status Processing Leave blank to disable

+ + +

Order Status Failed

+

Enter your template for WooCommerce Order Status Failed Leave blank to disable

+ + + + + + +

Order Status Draft

+

Enter your template for WooCommerce Order Status Draft Leave blank to disable

+ + + + + + +

Order Status Refunded

+

Enter your template for WooCommerce Order Status Refunded Leave blank to disable

+ + + + + + +

Order Status Cancelled

+

Enter your template for WooCommerce Order Status Cancelled Leave blank to disable

+ + + + + + +

Order Status On Hold

+

Enter your template for WooCommerce Order Status On Hold Leave blank to disable

+ + + + + + +
+
+
+ +
+
+

Backup created successfully! Refresh to see new files below.

'; + } elseif ($_GET['backup'] === 'failed') { + echo '

Backup failed — check debug log. Try unchecking some folders if memory is low.

'; + } + } + ?> + + +
+ +

Backup Settings

+

Configure what to include in backups. Uncheck large folders if you have memory limits.

+ +
+
+ + + +
+ + +
+ + + +

Creates database export + selected folders. May take time on large sites.

+
+ + Backup list function not available. Check if dapper_backup.php loaded correctly (syntax error?).

'; + } + ?> +
+ + + + + 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'); + // 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 +} + +/* +// 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') { + + + ?> + + '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; +} diff --git a/dapper_backup.php b/dapper_backup.php new file mode 100644 index 0000000..9f4f1b2 --- /dev/null +++ b/dapper_backup.php @@ -0,0 +1,402 @@ +get_col('SHOW TABLES'); + $chunk_size = 300; // Smaller chunks for safety + + foreach ($tables as $table) { + $create = $wpdb->get_row("SHOW CREATE TABLE `$table`", ARRAY_N); + fwrite($handle, $create[1] . ";\n\n"); + + $offset = 0; + while (true) { + $rows = $wpdb->get_results($wpdb->prepare("SELECT * FROM `$table` LIMIT %d OFFSET %d", $chunk_size, $offset), ARRAY_A); + if (empty($rows)) break; + + foreach ($rows as $row) { + $values = array_map([$wpdb, '_real_escape'], array_values($row)); + $insert = "INSERT INTO `$table` VALUES ('" . implode("','", $values) . "');\n"; + fwrite($handle, $insert); + } + + $offset += $chunk_size; + unset($rows); + gc_collect_cycles(); + + $mem = memory_get_usage(true) / 1024 / 1024; + if ($mem > 110) { + fclose($handle); + @unlink($file); + dapper_debug_log('Memory high during DB export — aborted'); + return false; + } + } + fwrite($handle, "\n\n"); + } + + fclose($handle); + dapper_debug_log('DB backup written to: ' . basename($file)); + return $file; +} + +/* +* Memory-safe zip to dated folder (optional folders) +*/ +function dapper_zip_essential_files($backup_folder) { + if (!dapper_ensure_backup_dir()) return false; + + $zip_files = []; // Collect successful zips + + $backup_folders = []; + + if (get_option('dapper_backup_themes', 'on') === 'on') { + $backup_folders['themes'] = WP_CONTENT_DIR . '/themes'; + } + if (get_option('dapper_backup_plugins', 'on') === 'on') { + $backup_folders['plugins'] = WP_CONTENT_DIR . '/plugins'; + } + if (get_option('dapper_backup_include_media', 'off') === 'on') { + $backup_folders['uploads'] = WP_CONTENT_DIR . '/uploads'; + } + + if (empty($backup_folders)) { + dapper_debug_log('No folders selected for file backup'); + return $zip_files; + } + + foreach ($backup_folders as $type => $root) { + if (!is_dir($root)) { + dapper_debug_log('Folder not found, skipping: ' . $type); + continue; + } + + $zip_file = $backup_folder . '/' . $type . '-' . date('Y-m-d_H-i-s') . '.zip'; + $zip = new ZipArchive(); + + if ($zip->open($zip_file, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== true) { + dapper_debug_log('Failed to open ' . $type . ' zip: ' . $zip_file); + continue; + } + + $file_count = 0; + $chunk_count = 0; + $max_per_chunk = 10000; // Reopen every 10k files to avoid timeout + + $iterator = new RecursiveIteratorIterator( + new RecursiveDirectoryIterator($root, RecursiveDirectoryIterator::SKIP_DOTS), + RecursiveIteratorIterator::LEAVES_ONLY + ); + + foreach ($iterator as $file) { + if ($file->isDir()) continue; + + $file_path = $file->getRealPath(); + $relative = substr($file_path, strlen(WP_CONTENT_DIR) + 1); + + $zip->addFile($file_path, $relative); + $file_count++; + $chunk_count++; + + if ($chunk_count % 500 === 0) { + $mem_mb = memory_get_usage(true) / 1024 / 1024; + dapper_debug_log("Added $file_count files to $type zip — memory: $mem_mb MB"); + } + + // Reopen zip every max_per_chunk files to reset buffers and avoid timeout + if ($chunk_count >= $max_per_chunk) { + $zip->close(); + dapper_debug_log("Chunk complete for $type ($file_count total) — reopening zip"); + $zip = new ZipArchive(); + if ($zip->open($zip_file, ZipArchive::CREATE) !== true) { // CREATE appends, doesn't overwrite + dapper_debug_log('Failed to reopen ' . $type . ' zip'); + break; + } + $chunk_count = 0; // Reset chunk counter + } + } + + $zip->close(); + + if (!file_exists($zip_file) || filesize($zip_file) < 1024) { + dapper_debug_log($type . ' zip empty or failed: ' . $zip_file); + @unlink($zip_file); + continue; + } + + dapper_debug_log($type . ' zip completed with ' . $file_count . ' files: ' . basename($zip_file)); + $zip_files[] = $zip_file; + } + + return $zip_files; +} + +/** + * Create full dated backup set + */ +function dapper_create_new_backup() { + $backup_folder = dapper_create_backup_folder(); + if (!$backup_folder) return false; + + $db_file = dapper_export_database($backup_folder); + $files_zip = dapper_zip_essential_files($backup_folder); + + if (!$db_file && !$files_zip) { + // Clean up empty folder if both failed + @rmdir($backup_folder); + return false; + } + + dapper_cleanup_old_backups(); + + return $backup_folder; +} + +/** + * Cleanup: keep only last N dated folders + */ +function dapper_cleanup_old_backups($keep = DAPPER_BACKUP_RETENTION) { + $folders = glob(DAPPER_BACKUP_BASE_DIR . '/*', GLOB_ONLYDIR); + if (count($folders) <= $keep) return; + + usort($folders, function($a, $b) { + return filemtime($b) <=> filemtime($a); + }); + + for ($i = $keep; $i < count($folders); $i++) { + // Delete folder recursively + array_map('unlink', glob("$folders[$i]/*")); + @rmdir($folders[$i]); + dapper_debug_log('Deleted old backup folder: ' . basename($folders[$i])); + } +} + +/** + * Get list of dated backups for display + */ +function dapper_get_old_backups() { + $folders = glob(DAPPER_BACKUP_BASE_DIR . '/*', GLOB_ONLYDIR); + $backups = []; + + foreach ($folders as $folder) { + $timestamp = basename($folder); + $ts = strtotime(str_replace('_', ' ', $timestamp)); + + $db_file = glob($folder . '/database-*.sql'); + $zip_file = glob($folder . '/files-*.zip'); + + $backups[] = [ + 'folder' => $folder, + 'timestamp' => $ts, + 'date_fmt' => date('Y-m-d H:i:s', $ts), + 'db' => !empty($db_file) ? $db_file[0] : '', + 'files' => !empty($zip_file) ? $zip_file[0] : '', + 'size_db' => !empty($db_file) ? filesize($db_file[0]) : 0, + 'size_files'=> !empty($zip_file) ? filesize($zip_file[0]) : 0, + ]; + } + + // Sort newest first + usort($backups, function($a, $b) { + return $b['timestamp'] <=> $a['timestamp']; + }); + + return $backups; +} + +/** + * Display backups in settings (dated folders) + */ + function display_old_backup_list() { + $folders = glob(DAPPER_BACKUP_BASE_DIR . '/*', GLOB_ONLYDIR); + if (empty($folders)) { + echo '

No backups yet.

'; + return; + } + + usort($folders, function($a, $b) { return filemtime($b) <=> filemtime($a); }); + + echo ''; + echo ''; + echo ''; + + foreach ($folders as $folder) { + $date = date('Y-m-d H:i:s', filemtime($folder)); + $files = glob($folder . '/*.{sql,zip}', GLOB_BRACE); + $total_size = 0; + $file_links = []; + + foreach ($files as $file) { + $size = filesize($file); + $total_size += $size; + $fname = basename($file); + $nonce = wp_create_nonce('dapper_dl_' . md5($fname)); + $link = admin_url('admin-post.php?action=dapper_backup_dl&f=' . urlencode($fname) . '&folder=' . urlencode(basename($folder)) . '&_wpnonce=' . $nonce); + $file_links[] = '' . esc_html($fname) . ' (' . size_format($size) . ')'; + } + + $delete_nonce = wp_create_nonce('dapper_delete_' . md5(basename($folder))); + $delete_link = admin_url('admin-post.php?action=dapper_delete_backup&folder=' . urlencode(basename($folder)) . '&_wpnonce=' . $delete_nonce); + + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + } + + echo '
DateFilesSizeActions
' . esc_html($date) . '' . implode('
', $file_links) . '
' . size_format($total_size) . 'Delete
'; + } + +// Secure download (updated for dated folders) +add_action('admin_post_dapper_backup_dl', 'dapper_handle_backup_download'); + +function dapper_handle_backup_download() { + if (!current_user_can('manage_options')) { + wp_die('Access denied.'); + } + + $file = isset($_GET['f']) ? basename($_GET['f']) : ''; + $folder = isset($_GET['folder']) ? basename($_GET['folder']) : ''; + $nonce = $_GET['_wpnonce'] ?? ''; + + if (!wp_verify_nonce($nonce, 'dapper_dl_' . md5($file))) { + wp_die('Security check failed.'); + } + + $path = DAPPER_BACKUP_BASE_DIR . '/' . $folder . '/' . $file; + if (!file_exists($path) || !is_readable($path)) { + wp_die('File not found or inaccessible.'); + } + + // Disable output buffering & compression for large files + if (ob_get_level()) ob_end_clean(); + header_remove('Content-Encoding'); // Prevent gzip + + // Proper headers for binary download + header('Content-Description: File Transfer'); + header('Content-Type: application/zip'); // Force zip type + header('Content-Disposition: attachment; filename="' . $file . '"'); + header('Content-Transfer-Encoding: binary'); + header('Expires: 0'); + header('Cache-Control: must-revalidate, post-check=0, pre-check=0'); + header('Pragma: public'); + + // Chunked output to avoid memory issues + header('Content-Length: ' . filesize($path)); + + // Stream file in 1 MB chunks + $chunk_size = 1024 * 1024; // 1 MB + $handle = fopen($path, 'rb'); + if ($handle === false) { + wp_die('Failed to open file for reading.'); + } + + while (!feof($handle)) { + echo fread($handle, $chunk_size); + flush(); // Send chunk immediately + } + + fclose($handle); + exit; +} + +// Delete entire dated backup folder +add_action('admin_post_dapper_delete_backup', 'dapper_handle_delete_backup'); + +function dapper_handle_delete_backup() { + if (!current_user_can('manage_options')) wp_die('Access denied.'); + + $folder = isset($_GET['folder']) ? basename($_GET['folder']) : ''; + $nonce = $_GET['_wpnonce'] ?? ''; + + if (!wp_verify_nonce($nonce, 'dapper_delete_' . md5($folder))) { + wp_die('Security check failed.'); + } + + $path = DAPPER_BACKUP_BASE_DIR . '/' . $folder; + if (!file_exists($path) || !is_dir($path)) { + wp_die('Backup not found.'); + } + + // Delete all files in folder + array_map('unlink', glob("$path/*")); + @rmdir($path); + + dapper_debug_log('Deleted backup folder: ' . $folder); + + $redirect = add_query_arg( + ['page' => 'dapper-settings', 'backup_deleted' => '1'], + admin_url('admin.php') + ); + wp_safe_redirect($redirect); + exit; +} diff --git a/readme.txt b/readme.txt new file mode 100644 index 0000000..c04b80c --- /dev/null +++ b/readme.txt @@ -0,0 +1,75 @@ +=== Dapper === +Contributors: williameothaw +Tags: woocommerce, email-automation, anti-spam, honeypot, custom-login-logo, mailing-list, email-templates, backup +Requires at least: 6.0 +Tested up to: 6.7 +Requires PHP: 8.0 +Stable tag: 1.1.1 +License: GPLv2 or later +License URI: https://www.gnu.org/licenses/gpl-2.0.html + +Custom WooCommerce utility: automated order emails, anti-spam protection, email templates/campaigns, mailing list, custom admin login logo, and basic backup tools. + +== Description == + +Dapper is a clean, dependency-free utility plugin for WooCommerce and WordPress sites that need reliable automation, security, and branding. + +**Current features include:** +- Automated, customizable order status emails (processing, shipped, delivered, tracking, review requests, etc.) +- Built-in anti-spam protection for registration and checkout (honeypot fields, JavaScript validation, submission speed limits — no third-party services) +- Custom email template and campaign post types for easy management +- Mailing list subscriber handling (add/remove/update status) +- **Custom WP-admin login logo** — upload your own 120×120 image in settings to brand the login screen (with live preview) +- Basic backup tab with manual backup creation, file listing, and secure downloads (database + essential files) +- Debug mode and conditional WooCommerce loading for performance + +**Upcoming:** +- Scheduled automated backups with retention policies and easy restore options +- Enhanced backup scheduling and notification features + +Perfect for managed hosting, client sites, or any WooCommerce store wanting secure, branded, no-bloat tools. + +== Installation == + +1. Upload the `dapper` folder to `/wp-content/plugins/` +2. Activate the plugin via the Plugins menu +3. Go to **Email Templates → Settings** to configure: + - Enable WooCommerce integration + - Upload custom login logo (120×120 px recommended) + - Set email templates, headers, review link structure, etc. + - Use the Backups tab for manual backups and downloads +4. Test order emails, checkout, login screen, and backups on a staging site first + +== Frequently Asked Questions == + += Can I brand the WordPress login screen? = +Yes — in Dapper Settings → General, enable "Custom Admin Panel Login Logo" and upload your image. It replaces the default logo on wp-login.php. + += Does it use any external services? = +No — all features run on your server (no APIs, no reCAPTCHA, no tracking pixels). + += How do backups work? = +The Backups tab lets you manually create a database export (.sql) and essential files zip. Downloads are secure and admin-only. Scheduled backups and restores coming soon. + +== Changelog == + += 1.1.1 - 2026-02-23 = +* Added Plugin URI and Author URI in header +* Implemented WooCommerce checkout anti-spam layers (honeypot fields, JS validation, submission speed limit) +* Added basic backup tab with manual creation, secure downloads, and file listing +* Formal changelog started (readme.txt short version + CHANGELOG.md full) +* Minor performance, code organization, and UI cleanups + += 1.1 / earlier = +* Core order status email automation +* Email template & campaign post types +* Mailing list management +* Custom admin login logo branding +* Many incremental improvements (not formally tracked pre-1.1.1) + +For full detailed history, see CHANGELOG.md in the plugin folder. + +== Upgrade Notice == + += 1.1.1 = +Adds checkout anti-spam protection, basic manual backups, and formal changelog. No database changes required. Recommended update — test checkout and backup creation on staging.