<?php
/*
Plugin Name: Auto Backup
Description: Automatically create versioned database backups at daily, weekly and monthly intervals.
Version: 1.04
Requires at least: 2.60
*/

// UPDATE THESE VALUES
$GLOBALS['AUTOBACKUP_DEFAULT_PREFIX']  = '';   // NEW! Set your backup file prefix here, or leave blank to use auto-detected domain from "Program Url" set in: Admin > General > Program Url
$GLOBALS['AUTOBACKUP_SKIP_TABLES']     = array('_cron_log','_error_log', '_outgoing_mail','_nlb_log'); // list of tables to skip

$GLOBALS['AUTOBACKUP_EMAIL']           = '';       // eg: user@example.com - If defined, backups will be emailed to this address (only the most frequent backup type will be sent)
$GLOBALS['AUTOBACKUP_EMAIL_FREQUENCY'] = 'daily'; // hourly, daily, weekly, or monthly

$GLOBALS['AUTOBACKUP_HOURS']  = 24;            // Keep automatic backups for the last X hours (max 24)
$GLOBALS['AUTOBACKUP_DAYS']   = 3;             // Keep automatic backups for the last X days  (max 7)
$GLOBALS['AUTOBACKUP_WEEKS']  = 3;             // Keep automatic backups for the last X weeks (max 52)
$GLOBALS['AUTOBACKUP_MONTHS'] = 3;             // Keep automatic backups for the last X months (max 12)

$GLOBALS['AUTOBACKUP_DIR']    = DATA_DIR . '/backups';

// DON'T UPDATE ANYTHING BELOW THIS LINE
$GLOBALS['AUTOBACKUP_PREFIX'] = _autoBackup_getPrefix();
$GLOBALS['AUTOBACKUP_SUFFIX'] = '.sql.php';

//**** DO NOT EDIT BEYOND THIS POINT ********//

addCronJob('autoBackup_updateBackups',    "Auto Backup", '0 59 23 * *');     // run every day
//addCronJob('autoBackup_updateBackups',    "Auto Backup", '* * * * *');     // DEUG run every minute
pluginAction_addHandlerAndLink(t('Run Manually'), 'autoBackup_updateBackups', 'admins'); // 

addFilter('backupDatabase_skippedTables', '_autoBackup_skipTables', null, 1);
addAction('section_init',                 '_autoBackup_showBackgroundTasksWarning', null, 2);

//
function autoBackup_updateBackups() {
  if (!inCLI()) {  // send headers (for running through web with "Run Manually" link above)
    if (!headers_sent()) { 
      header("Content-type: text/plain");
      header("Content-Disposition: inline; filename='output.txt'"); // Force IE to display as text and not download file
      //ob_disable();              // Turn off browser buffering
    }
    ignore_user_abort( true ); // continue running even if user clicks stop on their browser
    session_write_close();     // v2.51 - End the current session and store session data so locked session data doesn't prevent concurrent access to CMS by user while backup in progress
  }
  set_time_limit(0);  // ignore PHP's max_execution_time directive
  
  // error checking
  if (!isInstalled()) { return; } // don't backup if software hasn't been installed yet.
  if (!is_dir($GLOBALS['AUTOBACKUP_DIR'])) {
    die("AUTOBACKUP_DIR '" .htmlencode($GLOBALS['AUTOBACKUP_DIR']). "' doesn't exists or isn't a directory! " .htmlencode(@$php_errormsg));
  }

  
  // create backup files
  $units = array('hourly', 'daily', 'weekly', 'monthly');
  $backupCount = 0;
  $successFilepath = ''; 
  foreach ($units as $unit) {
    $filename = _autoBackup_getBackupFileFor($unit);
    $filepath = "{$GLOBALS['AUTOBACKUP_DIR']}/$filename";
    
    // if file already exists, check if it's new or old
    if (file_exists($filepath)) {
      $mtime = filemtime($filepath);
      $isNew = false;
      if (!$isNew && $unit == 'hourly'  && $mtime > strtotime("-1 hour"))  { $isNew = true; }
      if (!$isNew && $unit == 'daily'   && $mtime > strtotime("-1 day"))   { $isNew = true; }
      if (!$isNew && $unit == 'weekly'  && $mtime > strtotime("-1 week"))  { $isNew = true; }
      if (!$isNew && $unit == 'monthly' && $mtime > strtotime("-1 month")) { $isNew = true; }
      if ($isNew) {
        print sprintf(t('Skipping existing \'%1$s\' backup: %2$s'), $unit, $filepath) . "\n"; 
        continue;
      } // don't backup over top of a backup that we just made
      @unlink($filepath);        // otherwise, erase old backup file (backupDatabase doesn't overwrite backup files)
    }

    // output status for cron log 
    $backupFilepath = $GLOBALS['AUTOBACKUP_DIR'] . "/$filename"; 
    
    if ($successFilepath) { // if we've already made a successful backup this session, just copy it rather than recreating it (much faster)
      echo sprintf( t('Copying backup file: %s'), $backupFilepath ) . "\n";
      copy($successFilepath, $backupFilepath) || die("Error copying '$successFilepath' to '$backupFilepath': " . @$php_errormsg);
      $successFilepath = $backupFilepath;
    }
    else { // otherwise, actually backup
      echo sprintf( t('Creating backup file: %s'), $backupFilepath ) . "\n";
      $successFilepath = backupDatabase($backupFilepath);
    }
    $backupCount++;
    
    // send email
    if ($successFilepath && $GLOBALS['AUTOBACKUP_EMAIL'] && $GLOBALS['AUTOBACKUP_EMAIL_FREQUENCY'] == $unit) {
      echo sprintf( t("Sending backup email to: %s"), $GLOBALS['AUTOBACKUP_EMAIL']) . "\n";
      _autoBackup_emailBackup($filename);
    }
  }

  // remove old backup files
  $backupFiles  = scandir( $GLOBALS['AUTOBACKUP_DIR'] );
  $allowedFiles = _autoBackup_getAllowedFiles();
  $regexp       = preg_quote($GLOBALS['AUTOBACKUP_PREFIX'], '/') . '(hourly|daily|weekly|monthly)-';
  foreach ($backupFiles as $filename) {
    if (!preg_match("/^$regexp/", $filename)) { continue; } // skip files not created by autoBackup
    if (in_array($filename, $allowedFiles)) { continue; }   // don't delete files that haven't expired

    $filepath = "{$GLOBALS['AUTOBACKUP_DIR']}/$filename";
    echo sprintf( t("Removing old backup file: %s"), $filepath). "\n";
    @unlink($filepath);
    if (file_exists($filepath)) { die("Error erasing '$filepath'! $php_errormsg"); }
  }
  
    echo sprintf( t("Skipped tables: %s"), implode(', ', $GLOBALS['AUTOBACKUP_SKIP_TABLES'])) . "\n";
//  if (!inCLI()) {
    echo sprintf(t("Backup complete! (%s seconds)"), showExecuteSeconds(true))  . "\n";
//  }

  // return summary message
  $summary = sprintf(t("%s backups"), $backupCount);
  if (@$_REQUEST['_pluginAction'] == __FUNCTION__) { exit; }  // exit if being run manually from web
  return $summary;  // otherwise, return summary if being run by cron
}


//
function _autoBackup_getBackupFileFor($unit, $time = '') {
  if (!$time) { $time = time(); }

  // get filename
  $filename = '';
  if     ($unit == 'hourly')  { $filename = 'hourly-'  . date('H', $time); }
  elseif ($unit == 'daily')   { $filename = 'daily-'   . date('D', $time); }
  elseif ($unit == 'weekly')  { $filename = 'weekly-'  . date('W', $time); }
  elseif ($unit == 'monthly') { $filename = 'monthly-' . date('M', $time); }
  else { die(__FUNCTION__ . ": Unknown unit '$unit'!"); }
  $filename = $GLOBALS['AUTOBACKUP_PREFIX'] . $filename . $GLOBALS['AUTOBACKUP_SUFFIX'];

  //
  return $filename;
}


//
function _autoBackup_getAllowedFiles() {

  // get list of allowed files
  $allowedFiles   = array();

  if ($GLOBALS['AUTOBACKUP_HOURS']) {
    foreach (range(0, $GLOBALS['AUTOBACKUP_HOURS']-1) as $multiple) {
      $allowedFiles[] = _autoBackup_getBackupFileFor('hourly', strtotime("-$multiple hours"));
    }
  }

  if ($GLOBALS['AUTOBACKUP_DAYS']) {
    foreach (range(0, $GLOBALS['AUTOBACKUP_DAYS']-1) as $multiple) {
      $allowedFiles[] = _autoBackup_getBackupFileFor('daily', strtotime("-$multiple days"));
    }
  }

  if ($GLOBALS['AUTOBACKUP_WEEKS']) {
    foreach (range(0, $GLOBALS['AUTOBACKUP_WEEKS']-1) as $multiple) {
      $allowedFiles[] = _autoBackup_getBackupFileFor('weekly', strtotime("-$multiple weeks"));
    }
  }

  if ($GLOBALS['AUTOBACKUP_WEEKS']) {
    foreach (range(0, $GLOBALS['AUTOBACKUP_MONTHS']-1) as $multiple) {
      $allowedFiles[] = _autoBackup_getBackupFileFor('monthly', strtotime("-$multiple months"));
    }
  }

  //
  return $allowedFiles;

}

// get backup filename prefix
function _autoBackup_getPrefix() {
  
  // get base prefix
  $hostname = parse_url(@$GLOBALS['SETTINGS']['adminUrl'], PHP_URL_HOST);
  $prefix   = coalesce($GLOBALS['AUTOBACKUP_DEFAULT_PREFIX'], $hostname, 'backup'); // use supplied prefix, hostname set in admin, or "backup"
  $prefix .= "-v{$GLOBALS['APP']['version']}-"; // add version

  //
  return $prefix;
}


//
function _autoBackup_emailBackup($filename) {
  if (!@$GLOBALS['AUTOBACKUP_EMAIL']) { return; }
  if (!isValidEmail($GLOBALS['AUTOBACKUP_EMAIL'])) { return; }

  // get backup data
  $filepath = "{$GLOBALS['AUTOBACKUP_DIR']}/$filename";
  $data     = file_get_contents($filepath);

  // compress
  $gzEncodeAvailable = function_exists('gzencode') && @gzencode('test');  // test gzencode is available and doesn't return error (seen faulty install returned 'stream error' on any usage
  if ($gzEncodeAvailable) {
    $data      = gzencode($data, 9);
    $filename .= '.gz';
  }

  // email attachment
  sendMessage(array(
    'from'    => $GLOBALS['SETTINGS']['adminEmail'],
    'to'      => $GLOBALS['AUTOBACKUP_EMAIL'],
    'subject' => "Auto Backup: $filename",
    'text'    => "Automatically generated backup file.",
    'attachments' => array(
      $filename  => $data,
    ),
  ));
}

//
function _autoBackup_skipTables($skippedTables) {
  $skippedTables = array_merge($skippedTables, $GLOBALS['AUTOBACKUP_SKIP_TABLES']); 
  return $skippedTables;
}


//
function _autoBackup_showBackgroundTasksWarning($tableName, $action) {
  if ($tableName != 'admin') { return; } // only show on admin menus
  
  // show warning if bgtasks not running
  $isBgTasksRunning = $GLOBALS['SETTINGS']['bgtasks_lastRun'] > strtotime('-61 minutes');
  if (!$isBgTasksRunning) { 
    $message  = sprintf(t("Auto Backup Warning: Please enable %sBackground Tasks%s so backups can run in background (no tasks have run in the last 60 minutes)."), "<a href='?menu=admin&action=general#background-tasks'>", "</a>") . "<br/>\n";
    alert($message);
  }

}

?>
