Initial commit
This commit is contained in:
2
.gitattributes
vendored
Normal file
2
.gitattributes
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
# Auto detect text files and perform LF normalization
|
||||||
|
* text=auto
|
||||||
40
CHANGELOG.md
Normal file
40
CHANGELOG.md
Normal file
@@ -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
|
||||||
|
|
||||||
|
...
|
||||||
32
admin.js
Normal file
32
admin.js
Normal file
@@ -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('<img src="' + image_url + '" alt="Custom Logo Preview" style="max-width: 100px; max-height: 100px;" />');
|
||||||
|
});
|
||||||
|
|
||||||
|
// 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('<img src="' + custom_logo_url + '" alt="Custom Logo Preview" style="max-width: 100px; max-height: 100px;" />');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
1259
dapper.php
Normal file
1259
dapper.php
Normal file
File diff suppressed because it is too large
Load Diff
402
dapper_backup.php
Normal file
402
dapper_backup.php
Normal file
@@ -0,0 +1,402 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Dapper Backup Module – Low-Memory, Dated Folders Version
|
||||||
|
*/
|
||||||
|
|
||||||
|
if (!defined('ABSPATH')) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
define('DAPPER_BACKUP_BASE_DIR', WP_CONTENT_DIR . '/dapper-backups');
|
||||||
|
define('DAPPER_BACKUP_RETENTION', 7); // keep last 7 full backup sets
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensure base backup directory exists and is protected
|
||||||
|
*/
|
||||||
|
function dapper_ensure_backup_dir() {
|
||||||
|
$base_dir = DAPPER_BACKUP_BASE_DIR;
|
||||||
|
if (!wp_mkdir_p($base_dir)) {
|
||||||
|
dapper_debug_log('Failed to create base backup dir: ' . $base_dir);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$htaccess = $base_dir . '/.htaccess';
|
||||||
|
if (!file_exists($htaccess)) {
|
||||||
|
file_put_contents($htaccess, "Order deny,allow\nDeny from all\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
$index = $base_dir . '/index.php';
|
||||||
|
if (!file_exists($index)) {
|
||||||
|
file_put_contents($index, '<?php // Silence is golden');
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new dated backup subfolder and return its path
|
||||||
|
*/
|
||||||
|
function dapper_create_backup_folder() {
|
||||||
|
if (!dapper_ensure_backup_dir()) return false;
|
||||||
|
|
||||||
|
$timestamp = date('Y-m-d_H-i-s');
|
||||||
|
$backup_folder = DAPPER_BACKUP_BASE_DIR . '/' . $timestamp;
|
||||||
|
|
||||||
|
if (!wp_mkdir_p($backup_folder)) {
|
||||||
|
dapper_debug_log('Failed to create dated backup folder: ' . $backup_folder);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Protect subfolder too
|
||||||
|
file_put_contents($backup_folder . '/index.php', '<?php // Silence is golden');
|
||||||
|
|
||||||
|
dapper_debug_log('Created dated backup folder: ' . $timestamp);
|
||||||
|
return $backup_folder;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Chunked database export to dated folder
|
||||||
|
*/
|
||||||
|
function dapper_export_database($backup_folder) {
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
$file = $backup_folder . '/database-' . date('Y-m-d_H-i-s') . '.sql';
|
||||||
|
$handle = fopen($file, 'w');
|
||||||
|
if ($handle === false) {
|
||||||
|
dapper_debug_log('Failed to open DB file: ' . $file);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
fwrite($handle, "-- Dapper Database Backup\n-- Generated: " . date('Y-m-d H:i:s') . "\n\n");
|
||||||
|
|
||||||
|
$tables = $wpdb->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 '<p>No backups yet.</p>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
usort($folders, function($a, $b) { return filemtime($b) <=> filemtime($a); });
|
||||||
|
|
||||||
|
echo '<table class="widefat striped">';
|
||||||
|
echo '<thead><tr><th>Date</th><th>Files</th><th>Size</th><th>Actions</th></tr></thead>';
|
||||||
|
echo '<tbody>';
|
||||||
|
|
||||||
|
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[] = '<a href="' . esc_url($link) . '">' . esc_html($fname) . '</a> (' . 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 '<tr>';
|
||||||
|
echo '<td>' . esc_html($date) . '</td>';
|
||||||
|
echo '<td>' . implode('<br>', $file_links) . '</td>';
|
||||||
|
echo '<td>' . size_format($total_size) . '</td>';
|
||||||
|
echo '<td><a href="' . esc_url($delete_link) . '" onclick="return confirm(\'Delete this backup set?\');">Delete</a></td>';
|
||||||
|
echo '</tr>';
|
||||||
|
}
|
||||||
|
|
||||||
|
echo '</tbody></table>';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
75
readme.txt
Normal file
75
readme.txt
Normal file
@@ -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.
|
||||||
Reference in New Issue
Block a user