WordPress Plugin: Create a Simple Daily Theme Backup

Over the last few months, I’ve been working hard to improve the performance of my WordPress child theme. Periodically, I’d introduce an issue in my child theme as I optimized it, and I needed to revert to an older copy of the code I’d updated in my theme. However, the daily backups on my host are over a gigabyte, so it takes time to download and search for the code I need to restore.

Losing your WordPress theme due to a mishap or malicious attack can be a nightmare. That’s why having a reliable backup system is crucial. Many plugins offer comprehensive backup solutions, but sometimes, you need a simple, targeted approach. This article delves into a custom WordPress plugin designed to automatically back up your active theme daily, providing an extra layer of protection for your site’s design and functionality.

NOTE: This is not a substitute for a full, offsite backup of your WordPress instance. It’s just an easy means to have a quickly accessible copy of your active theme. This saves up to 10 instances so that you don’t fill up your host with backups.

Daily Theme Backup WordPress Plugin

I created a Daily Theme Backup plugin that takes your active theme, zips it up, and puts it into a backup folder on your site. It also makes that folder inaccessible to external viewers so that you can ensure the files aren’t downloaded by anyone.

To use this, add a folder to your wp-content/plugins folder called daily-theme-backup and copy the following code into a file in that directory, daily-theme-backup.php.

<?php
/**
 * Plugin Name: Daily Theme Backup
 * Description: Creates a daily backup of your active theme and stores it in the wp-content/backup directory.
 * Version: 3.0
 * Author: Douglas Karr
 * Author URI: https://dknewmedia.com
 */

// Create the backup directory if it doesn't exist.
if (!file_exists(WP_CONTENT_DIR . '/backup')) {
  mkdir(WP_CONTENT_DIR . '/backup', 0755);
}

// Function to create the .htaccess file for backup directory protection
function protect_backup_directory() {
  $htaccess_file = WP_CONTENT_DIR . '/backup/.htaccess';
  $htaccess_content = 'deny from all';

  if (!file_exists($htaccess_file)) {
    if (insert_with_markers($htaccess_file, 'Daily Theme Backup', $htaccess_content)) {
      // Optional: Log success or display a message
      error_log('Backup directory protected with .htaccess.');
    } else {
      // Optional: Log error or display a message
      error_log('Error creating .htaccess file for backup directory protection.');
    }
  }
}

// Run the protect_backup_directory function on plugin activation.
register_activation_hook( __FILE__, 'protect_backup_directory' );

// Schedule the daily backup.
if (!wp_next_scheduled('daily_theme_backup')) {
  wp_schedule_event(time(), 'daily', 'daily_theme_backup');
}

// Hook the backup function to the scheduled event.
add_action('daily_theme_backup', 'create_theme_backup');

// Run the backup function once on plugin activation.
register_activation_hook( __FILE__, 'create_theme_backup' ); 

// Add a "Backup Now" link to the plugin actions.
add_filter( 'plugin_action_links_' . plugin_basename(__FILE__), 'add_theme_backup_action_link' );

function add_theme_backup_action_link( $links ) {
  $backup_link = '<a href="' . esc_url( add_query_arg( 'backup_now', 'true' ) ) . '">Backup Now</a>';
  array_unshift( $links, $backup_link ); // Add the link with a separator
  return $links;
}

// Check for the "backup_now" query parameter and trigger the backup.
if ( isset( $_GET['backup_now'] ) && $_GET['backup_now'] == 'true' ) {
  create_theme_backup();

  // Optionally, display a success message.
  add_action( 'admin_notices', 'display_backup_success_message' );
}

function display_backup_success_message() {
  echo '<div class="notice notice-success is-dismissible"><p>Theme backup created successfully!</p></div>';
}

function create_theme_backup() {
  // Get the active theme directory.
  $theme_dir = get_stylesheet_directory();
  $theme_name = basename($theme_dir);

  // Create the backup filename with timestamp if triggered manually.
  if ( isset( $_GET['backup_now'] ) && $_GET['backup_now'] == 'true' ) {
    $backup_file = WP_CONTENT_DIR . '/backup/' . $theme_name . '-' . date('Y-m-d-H-i-s') . '.zip';
  } else {
    $backup_file = WP_CONTENT_DIR . '/backup/' . $theme_name . '-' . date('Y-m-d') . '.zip';
  }

  // Create the zip archive.
  $zip = new ZipArchive();
  if ($zip->open($backup_file, ZipArchive::CREATE) === TRUE) {
    // Add the theme directory to the zip archive.
    $files = new RecursiveIteratorIterator(
      new RecursiveDirectoryIterator($theme_dir),
      RecursiveIteratorIterator::LEAVES_ONLY
    );

    foreach ($files as $name => $file) {
      // Skip directories (they would be added automatically)
      if (!$file->isDir()) {
        // Get real and relative path for current file
        $filePath = $file->getRealPath();
        $relativePath = substr($filePath, strlen($theme_dir) + 1);

        // Add current file to archive
        $zip->addFile($filePath, $relativePath);
      }
    }

    $zip->close();

    // Delete old backups.
    $backup_files = glob(WP_CONTENT_DIR . '/backup/' . $theme_name . '-*.zip');
    if (count($backup_files) > 10) {
      usort($backup_files, 'filemtime_compare');
      for ($i = 0; $i < count($backup_files) - 10; $i++) {
        unlink($backup_files[$i]);
      }
    }
  } else {
    // Log an error if the zip archive could not be created.
    error_log('Error creating theme backup.');
  }
}

// Helper function to compare file modification times.
function filemtime_compare($a, $b) {
  return filemtime($a) - filemtime($b);
}
?>

The Code: A Breakdown

This plugin utilizes PHP and WordPress’s built-in functions to achieve its goal. Let’s break down the key components:

1. Setting the Stage:

// Create the backup directory if it doesn't exist.
if (!file_exists(WP_CONTENT_DIR . '/backup')) {
  mkdir(WP_CONTENT_DIR . '/backup', 0755);
}

// Function to create the .htaccess file for backup directory protection
function protect_backup_directory() {
  $htaccess_file = WP_CONTENT_DIR . '/backup/.htaccess';
  $htaccess_content = 'deny from all';

  if (!file_exists($htaccess_file)) {
    if (insert_with_markers($htaccess_file, 'Daily Theme Backup', $htaccess_content)) {
      error_log('Backup directory protected with .htaccess.');
    } else {
      error_log('Error creating .htaccess file for backup directory protection.');
    }
  }
}

// Run the protect_backup_directory function on plugin activation.
register_activation_hook( __FILE__, 'protect_backup_directory' );

2. Scheduling the Backup:

// Schedule the daily backup.
if (!wp_next_scheduled('daily_theme_backup')) {
  wp_schedule_event(time(), 'daily', 'daily_theme_backup');
}

// Hook the backup function to the scheduled event.
add_action('daily_theme_backup', 'create_theme_backup');

3. The Backup Process:

function create_theme_backup() {
  // Get the active theme directory.
  $theme_dir = get_stylesheet_directory();
  $theme_name = basename($theme_dir);

  // Create the backup filename with timestamp if triggered manually.
  if ( isset( $_GET['backup_now'] ) && $_GET['backup_now'] == 'true' ) {
    $backup_file = WP_CONTENT_DIR . '/backup/' . $theme_name . '-' . date('Y-m-d-H-i-s') . '.zip';
  } else {
    $backup_file = WP_CONTENT_DIR . '/backup/' . $theme_name . '-' . date('Y-m-d') . '.zip';
  }

  // Create the zip archive.
  $zip = new ZipArchive();
  if ($zip->open($backup_file, ZipArchive::CREATE) === TRUE) {
    // Add the theme directory to the zip archive.
    $files = new RecursiveIteratorIterator(
      new RecursiveDirectoryIterator($theme_dir),
      RecursiveIteratorIterator::LEAVES_ONLY
    );

    foreach ($files as $name => $file) {
      if (!$file->isDir()) {
        $filePath = $file->getRealPath();
        $relativePath = substr($filePath, strlen($theme_dir) + 1);
        $zip->addFile($filePath, $relativePath);
      }
    }

    $zip->close();

    // Delete old backups.
    $backup_files = glob(WP_CONTENT_DIR . '/backup/' . $theme_name . '-*.zip');
    if (count($backup_files) > 10) {
      usort($backup_files, 'filemtime_compare');
      for ($i = 0; $i < count($backup_files) - 10; $i++) {
        unlink($backup_files[$i]);
      }
    }
  } else {
    error_log('Error creating theme backup.');
  }
}

// Helper function to compare file modification times.
function filemtime_compare($a, $b) {
  return filemtime($a) - filemtime($b);
}

4. On-Demand Backups:

// Add a "Backup Now" link to the plugin actions.
add_filter( 'plugin_action_links_' . plugin_basename(__FILE__), 'add_theme_backup_action_link' );

function add_theme_backup_action_link( $links ) {
  $backup_link = '<a href="' . esc_url( add_query_arg( 'backup_now', 'true' ) ) . '">Backup Now</a>';
  array_unshift( $links, $backup_link . ' | ' ); 
  return $links;
}

// Check for the "backup_now" query parameter and trigger the backup.
if ( isset( $_GET['backup_now'] ) && $_GET['backup_now'] == 'true' ) {
  create_theme_backup();
  add_action( 'admin_notices', 'display_backup_success_message' );
}

function display_backup_success_message() {
  echo '<div class="notice notice-success is-dismissible"><p>Theme backup created successfully!</p></div>';
}

Why This Matters

This plugin provides a focused solution for safeguarding your WordPress theme. Automating the backup process and securing the backup files offers peace of mind and a quick recovery option in case of unexpected issues. Understanding this code empowers you to customize it further, adapting it to your specific needs or expanding it to back up other crucial elements of your WordPress site.

Exit mobile version