diff --git a/dapper.php b/dapper.php index 97b1283..9c3d90e 100644 --- a/dapper.php +++ b/dapper.php @@ -38,6 +38,8 @@ function dapper_activation() { 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 @@ -47,47 +49,48 @@ function dapper_activation() { register_activation_hook(__FILE__, 'dapper_activation'); /** - * Get short Git commit hash (8 chars) as build identifier. - * Safe fallback if .git is missing (e.g. on production deploy). + * 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_git_build() { +function dapper_get_build_identifier() { static $build = null; - if ($build !== null) return $build; - - $git_dir = DAPPER_PATH . '.git'; - if (!is_dir($git_dir)) { - $build = 'dev-local'; + if ( $build !== null ) { return $build; } - $head_path = $git_dir . '/HEAD'; - if (!file_exists($head_path)) { - $build = 'git-head-missing'; - return $build; - } + $info_file = DAPPER_PATH . 'build-info.php'; - $head_content = trim(file_get_contents($head_path)); + 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']; + } - if (preg_match('#^ref: (refs/heads/.+)$#', $head_content, $matches)) { - $ref_path = $git_dir . '/' . $matches[1]; - if (file_exists($ref_path)) { - $commit = trim(file_get_contents($ref_path)); - $build = substr($commit, 0, 8); return $build; } } - // Detached HEAD fallback - if (strlen($head_content) >= 40 && preg_match('/^[0-9a-f]{40}$/', $head_content)) { - $build = substr($head_content, 0, 8); - 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 = 'git-unknown'; + $build = 'dev-local'; return $build; } -define('DAPPER_BUILD', dapper_get_git_build()); +define( 'DAPPER_BUILD', dapper_get_build_identifier() ); /* // Depreciated in v1.1.1 to be removed in later versions @@ -1095,12 +1098,148 @@ function dapper_woo_conditional_load() { } } + // 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 = ' + + +

+ +

'; + + // Insert just before + $form = str_replace('', $hidden_fields . '', $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; + ?> + + 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.

@@ -1282,7 +1418,20 @@ function dapper_settings_page_content() {

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

> - +

Paypal Human Verification Checkbox

+ + + + > @@ -1448,13 +1597,27 @@ function dapper_settings_page_content() { add_action('admin_menu', 'dapper_settings_page'); +/* +// Load media uploader and custom admin JS only on Dapper settings page +*/ function enqueue_media_uploader_scripts() { - if (isset($_GET['page']) && $_GET['page'] === 'dapper-settings') { + // 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(); - wp_enqueue_script('my-admin-js', plugin_dir_url(__FILE__) . 'admin.js', array('jquery'), null, false); + + // 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'); + +add_action( 'admin_enqueue_scripts', 'enqueue_media_uploader_scripts' ); /* // Retrieves the mailing list user with search and pagination @@ -1541,6 +1704,44 @@ function get_email_template_content_by_name($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 diff --git a/dapper_backup.php b/dapper_backup.php index 9f4f1b2..fc8f098 100644 --- a/dapper_backup.php +++ b/dapper_backup.php @@ -321,51 +321,56 @@ 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.'); + wp_die('Access denied.', 403); } - $file = isset($_GET['f']) ? basename($_GET['f']) : ''; + $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.'); + wp_die('Security check failed.', 403); } $path = DAPPER_BACKUP_BASE_DIR . '/' . $folder . '/' . $file; if (!file_exists($path) || !is_readable($path)) { - wp_die('File not found or inaccessible.'); + wp_die('File not found or not readable.', 404); } - // Disable output buffering & compression for large files - if (ob_get_level()) ob_end_clean(); - header_remove('Content-Encoding'); // Prevent gzip + // Kill all output buffering + while (ob_get_level() > 0) { + ob_end_clean(); + } - // Proper headers for binary download + // Turn off compression + if (function_exists('apache_setenv')) { + @apache_setenv('no-gzip', 1); + } + @ini_set('zlib.output_compression', 'Off'); + + // Headers 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('Content-Type: application/zip'); + header('Content-Disposition: attachment; filename="' . $file . '";'); header('Expires: 0'); - header('Cache-Control: must-revalidate, post-check=0, pre-check=0'); + header('Cache-Control: must-revalidate'); 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'); + // Chunked read — very memory efficient + $chunk = 1 * 1024 * 1024; // 1 MB + $handle = @fopen($path, 'rb'); if ($handle === false) { - wp_die('Failed to open file for reading.'); + wp_die('Cannot open file for reading.'); } while (!feof($handle)) { - echo fread($handle, $chunk_size); - flush(); // Send chunk immediately + echo @fread($handle, $chunk); + flush(); + if (connection_aborted()) break; } - fclose($handle); + @fclose($handle); exit; }