#!/usr/bin/php-cli -d log_errors=Off -d display_errors=On
<?php
 
error_reporting(E_ALL);
 
/*
 
cpbackup-mango.php - run a cPanel scheduled full backup and then rsync it wherever the user wants.
 
Usage: cpbackup-mango.php [--debug] [user1 [user2 [user3 [user4]]]]
If usernames are not specified, the script will loop through directories in $home.
The user must have a file called cpbackup-scheduled.conf in their home directory.  Here is an example of this file:
 
ssh_user=john_smith
ssh_host=example.com
ssh_directory=full_backups
max_backups=4
email=john_smith@example.com
 
Note that you do not specify a password in your config file.  Instead, set up an RSA key for authentication.
 
The latest version of this script may be found at http://www.toao.net/
 
*/
 
// $admin_email - set this to your email address so that you may receive reports of errors.
$admin_email = 'user@example.com';
// $from_email - set this to whatever emails to users and admins will appear from.
$from_email = '"Mango\'s Scheduled Full Backup" ';
// $debug - use the --debug switch to print general information to the console.  Note that if this is on, admins will NOT be emailed in the event of an error.
$debug = (in_array('--debug', $argv)) ? 1 : 0;
// $home - set this to the path to where the home directories are stored.  This typically doesn't need to be changed.
$home = '/home';
// $backup_location - the location where the backup files will temporarily be stored.  This folder should be owned by root with permissions 0700.
// NOTE: Anything named *.tar.gz in this folder will be deleted!
$backup_location = '/home/cpbackups-scheduled';
 
//////////////////////////
// And now for the script!
 
// Be sure $backup_location exists and has permissions 0700.
if (!is_dir($backup_location)) mkdir($backup_location, 0700, true); else chmod($backup_location, 0700);
// Delete any old backups left behind by accident.
better_exec("rm -f $backup_location/*.tar.gz");
 
// What users are we doing?
$users = array();
if (isset($argv[1])) {
 // Users may have been specified on the command line.
 for ($i = 1; $i < count($argv); $i++) if (is_dir("$home/{$argv[$i]}") or substr($argv[$i], 0, 1) != "-") $users[] = $argv[$i];
 }
if (count($users) == 0) {
 // Users were not specified on the command line - look for them in $home.
 if (!$handle = opendir($home)) error("Unable to open $home.  Backups will NOT continue."); 
 while (false !== ($user = readdir($handle))) if ($user != '..' and $user != '.' and is_dir("$home/$user")) $users[] = $user;
 closedir($handle);
 }
 
// Loop through the users in $home and find out which ones have scheduled full backup enabled.
foreach ($users as $user) {
 if (!is_file("$home/$user/cpbackup-scheduled.conf")) {
  debug("Checking $user ...scheduled full backup not enabled.");
  continue;
  }
 // This user has enabled a scheduled full backup.  How have they configured it?
 debug("Scheduled full backup enabled for user $user!");
 $config = array();
 $raw_config = preg_split("/\r\n|\r|\n/", file_get_contents("$home/$user/cpbackup-scheduled.conf"));
 foreach ($raw_config as $option) if (strpos($option, '=') !== false) {
  list($key, $value) = explode('=', $option, 2);
  $config[$key] = $value;
  if (preg_match("/[^\w@\-~.]/", $value)) {
   error_user("Illegal character found in cpbackup-scheduled.conf file.  The only characters allowed are: letters, numbers, _ (underscore), @ (at sign), - (dash), ~ (tilde), and . (period).");
   continue 2;
   }
  }
 // Make sure required configuration exists.
 if ((!isset($config['ssh_user']) or strlen($config['ssh_user']) == 0) or (!isset($config['ssh_host']) or strlen($config['ssh_host']) == 0)) {
  error_user("ssh_user and/or ssh_host is not defined in cpbackup-scheduled.conf file.  Backup for account $user will not be run this time.");
  continue;
  }
 if (!isset($config['ssh_directory'])) $config['ssh_directory'] = ''; else rtrim($config['ssh_directory'], '/');
 debug("Config:\n" . print_r($config,1));
 // Run the full backup.
 list($output, $error, $return) = better_exec("/scripts/pkgacct $user $backup_location backup");
 if ($error or $return != 0) {
  // Error running the backup.
  warn("cPanel reported a problem creating the full backup for $user.  The script will try to continue, but the backup may or may not have completed correctly.\nOutput: $output\nError: $error\nReturn value: $return");
  }
 // Rename the file.
 preg_match("/(?<=pkgacctfile is: ).*/", $output, $matches);
 if (!isset($matches[0]) or strpos($matches[0], '.tar.gz') === false) {
  warn("Created a backup for $user, but unable to find out the name of the backup file.  This backup failed, but we will try to continue any remaining ones, if they exist.\n\n$output");
  continue;
  }
 $backup_filename = "$user-backup-" . date('YmMd-H.i.s') . ".tar.gz";
 $backup_path_file = "$backup_location/$backup_filename";
 rename($matches[0], $backup_path_file);
 debug("Backup file was: {$matches[0]} and has been renamed to $backup_path_file.");
 // Make the directory.  If it already exists, mkdir will quit silently because of the -p switch.
 if ($config['ssh_directory']) better_exec("ssh -i $home/$user/.ssh/id_rsa -o 'BatchMode yes' {$config['ssh_user']}@{$config['ssh_host']} mkdir -p {$config['ssh_directory']}", 1);
 // rsync the file to $config['ssh_host'].
 list($output, $error, $return) = better_exec("rsync -e \"ssh -i $home/$user/.ssh/id_rsa\" $backup_path_file {$config['ssh_user']}@{$config['ssh_host']}:{$config['ssh_directory']}");
 if($error or $return != 0) {
  error_user("The backup file for $user could not be sent to the remote server.  The error will be shown below.\n\n$error");
  continue;
  }
 // Delete the file.
 if (!@unlink($backup_path_file)) warn("Unable to delete $backup_path_file.");
 // Delete old backups, if the user would like that.  First, get a listing of files in the directory we will be working with.
 if (isset($config['max_backups']) and $config['max_backups'] > 0) {
  list($files, $error, $return) = better_exec("ssh -i $home/$user/.ssh/id_rsa -o 'BatchMode yes' {$config['ssh_user']}@{$config['ssh_host']} ls -1t {$config['ssh_directory']}/*-backup-*.tar.gz");
  if($error or $return != 0) {
   error_user("Unable to get directory listing for {$config['ssh_directory']}/*-backup-*.tar.gz.  Cannot delete $user's old backups.");
   continue;
   }
  $files = preg_split("/\r\n|\r|\n/", trim($files));
  if (count($files) > $config['max_backups']) {
   debug("Listing of files: " . print_r($files, true));
   // Delete oldest files if there are more than max_backups files.
   $files_to_delete = '';
   for ($i = count($files)-1; $i > $config['max_backups']-1; $i=$i-1) if (preg_match("/\w/", $files[$i])) $files_to_delete .= "'{$files[$i]}' ";
   if ($files_to_delete) {
    debug("Files to delete: $files_to_delete");
    better_exec("ssh -i $home/$user/.ssh/id_rsa -o 'BatchMode yes' {$config['ssh_user']}@{$config['ssh_host']} \"rm $files_to_delete\"", 1);
    }
   } else {
   debug ('No files to delete');
   }
  }
 }
 
function error_user($message) {
 debug("USER ERROR: $message");
 // Log the error to a file.
 $fp = fopen("{$GLOBALS['home']}/{$GLOBALS['user']}/cpbackup-scheduled-error.log", 'a');
 fwrite($fp, date('[Y-M-d H:i:s]') . ' ' . trim($message) . "\n");
 fclose($fp);
 // Be sure the error log is owned by the user, and the user has permissions to open it.
 chown("{$GLOBALS['home']}/{$GLOBALS['user']}/cpbackup-scheduled-error.log", $GLOBALS['user']);
 chmod("{$GLOBALS['home']}/{$GLOBALS['user']}/cpbackup-scheduled-error.log", 0700);
 // Email the error to the user, if they have requested it.
 if (isset($GLOBALS['config']['email'])) {
  mail($GLOBALS['config']['email'], "Scheduled Full Backup: Error","The following error was encountered while trying to perform the backup:\n\n$message", "From: {$GLOBALS['from_email']}");
  }
 }
 
function warn($message) {
 error($message, 'WARNING');
 }
 
function error($message, $level='ERROR') {
 if ($GLOBALS['debug']) {
  // Do not email if debug is turned on; output to screen.
  debug("$level: $message");
  if ($level=='ERROR') exit; else return;
  }
 mail($GLOBALS['admin_email'], "Scheduled Full Backup: $level","The following problem was encountered while trying to perform the backup:\n\n$message", "From: {$GLOBALS['from_email']}");
 if ($level=='ERROR') exit;
 }
 
function debug($message) {
 if ($GLOBALS['debug']) echo "DEBUG: " . trim($message) . "\n";
 }
 
function better_exec($command) {
 // Execute $command and return the output, error text, and return value.
 debug($command);
 $process = proc_open($command, array(0 => array("pipe", "r"), 1 => array("pipe", "w"), 2 => array("pipe", "w")), $pipes);
 if (!is_resource($process)) error("Unable to run command:\n$command");
 fclose($pipes[0]);
 $output = stream_get_contents($pipes[1]);
 $error = trim(stream_get_contents($pipes[2]));
 fclose($pipes[1]); fclose($pipes[2]);
 $return = proc_close($process);
 return array($output, $error, $return);
 }
 
?>