<?php
// $Id: backup_migrate.module,v 1.1.2.32 2009/01/18 02:52:47 ronan Exp $


/**
 * @file
 * Create (manually or scheduled) and restore backups of your Drupal MySQL
 * database with an option to exclude table data (f.e. cache_*)
 */

/**
 * Implementation of hook_menu().
 */
function backup_migrate_menu($may_cache) {
  $items = array();

  if ($may_cache) {
    $items[] = array(
      'path' => 'admin/content/backup_migrate',
      'title' => t('Backup and Migrate'),
      'description' => t('Backup/restore your database or migrate data to or from another Drupal site.'),
      'callback' => 'drupal_get_form',
      'callback arguments' => array('backup_migrate_backup'),
      'access' => user_access('perform backup'),
      'type' => MENU_NORMAL_ITEM,
    );
    $items[] = array(
      'path' => 'admin/content/backup_migrate/export',
      'title' => t('Backup/Export DB'),
      'description' => t('Backup the database.'),
      'callback' => '_backup_migrate_export',
      'access' => user_access('perform backup'),
      'weight' => 0,
      'type' => MENU_DEFAULT_LOCAL_TASK,
    );
    $items[] = array(
      'path' => 'admin/content/backup_migrate/restore',
      'title' => t('Restore/Import DB'),
      'description' => t('Restore the database from a previous backup'),
      'callback' => 'drupal_get_form',
      'callback arguments' => array('backup_migrate_restore'),
      'access' => user_access('restore from backup'),
      'weight' => 1,
      'type' => MENU_LOCAL_TASK,
    );
    $items[] = array(
      'path' => 'admin/content/backup_migrate/files',
      'title' => t('Saved Backups'),
      'description' => t('View existing backup files'),
      'callback' => '_backup_migrate_list_files',
      'access' => user_access('access backup files'),
      'weight' => 2,
      'type' => MENU_LOCAL_TASK,
    );
    $items[] = array(
      'path' => 'admin/content/backup_migrate/files/manual',
      'title' => t('Manual Backups'),
      'callback' => '_backup_migrate_list_files',
      'callback arguments' => array('manual'),
      'access' => user_access('access backup files'),
      'weight' => 1,
      'type' => MENU_DEFAULT_LOCAL_TASK,
    );
    $items[] = array(
      'path' => 'admin/content/backup_migrate/files/scheduled',
      'title' => t('Scheduled Backups'),
      'callback' => '_backup_migrate_list_files',
      'callback arguments' => array('scheduled'),
      'access' => user_access('access backup files'),
      'weight' => 2,
      'type' => MENU_LOCAL_TASK,
    );
    $items[] = array(
      'path' => 'admin/content/backup_migrate/schedule',
      'title' => t('Backup Schedule'),
      'description' => t('View existing backup files'),
      'callback' => 'drupal_get_form',
      'callback arguments' => array('backup_migrate_schedule'),
      'access' => user_access('access backup files'),
      'weight' => 3,
      'type' => MENU_LOCAL_TASK,
    );
    $items[] = array(
      'path' => 'admin/content/backup_migrate/restorefile',
      'title' => t('restore from backup'),
      'description' => t('Restore database from a backup file on the server'),
      'callback' => '_backup_migrate_restore_from_server',
      'access' => user_access('restore from backup'),
      'type' => MENU_CALLBACK,
    );
    $items[] = array(
      'path' => 'admin/content/backup_migrate/delete',
      'title' => t('Delete File'),
      'description' => t('Delete a backup file'),
      'callback' => '_backup_migrate_delete',
      'access' => user_access('delete backup files'),
      'type' => MENU_CALLBACK,
    );
  }

  return $items;
}

/**
 * Implementation of hook_cron(),
 *
 * Takes care of scheduled backups.
 */
function backup_migrate_cron() {
  if ($backup_schedule_period = variable_get("backup_migrate_schedule_backup_period", 0)) {
    $last_backup = variable_get("backup_migrate_schedule_last_backup", 0);
    $now = time();
    if ($last_backup < ($now - ($backup_schedule_period * 60 * 60))) {
      // time to run the backup
      _backup_migrate_dump_tables(
        variable_get("backup_migrate_file_name", _backup_migrate_default_file_name()),
        variable_get("backup_migrate_exclude_tables", _backup_migrate_default_exclude_tables()),
        variable_get("backup_migrate_nodata_tables", _backup_migrate_default_structure_only_tables()),
        'sql',
        "save",
        variable_get("backup_migrate_compression", "none"),
        "scheduled",
        variable_get("backup_migrate_timestamp_format", 'Y-m-d\TH-i-s')
        );

      // Set the timestamp to indecate last backup time.
      variable_set("backup_migrate_schedule_last_backup", $now);

      // Delete older backups if needed.
      _backup_migrate_remove_expired_backups();
    }
  }

  // Delete temp files abandoned for 6 or more hours.
  foreach (file_scan_directory(file_directory_temp(), 'backup_migrate_*', array('.', '..'), 0, FALSE) as $file) {
    if (filectime($file->filename) < time() - 21600) {
      unlink($file->filename);
    }
  }
}

/**
 * Implementation of hook_exit().
 */
function backup_migrate_exit() {
  // Delete any temporary files generated during this execution.
  _backup_migrate_temp_file('', TRUE);
}

/**
 * Implementation of hook_perm().
 */
function backup_migrate_perm() {
  return array('perform backup', 'access backup files', 'delete backup files', 'restore from backup');
}

/**
 * Implementation of hook_simpletest().
 */
function backup_migrate_simpletest() {
  $dir = drupal_get_path('module', 'backup_migrate') .'/tests';
  $tests = file_scan_directory($dir, '\.test$');
  return array_keys($tests);
}

/**
 * Menu callback. Delete a previous backup.
 */
function _backup_migrate_delete() {
  // Merge remainder of arguments from GET['q'], into relative file path.
  $args = func_get_args();
  $path = implode('/', $args);

  if ($path && _backup_migrate_path_is_in_save_dir($path)) {
    return drupal_get_form('backup_migrate_delete_confirm', $path);
  }
  return user_access('access backup files') ? "admin/content/backup_migrate/files" : "admin/content/backup_migrate";
}

/**
 * Ask confirmation for file deletion.
 */
function backup_migrate_delete_confirm($path) {
  $form['path'] = array('#type' => 'value', '#value' => $path);
  return confirm_form($form, t('Are you sure you want to delete the backup file at %path?', array('%path' => $path)), 'admin/content/backup_migrate/files', t('This action cannot be undone.'), t('Delete'), t('Cancel'));
}

function backup_migrate_delete_confirm_submit($form_id, $form_values) {
  $path = $form_values['path'];

  if ($path && _backup_migrate_path_is_in_save_dir($path)) {
    file_delete($path);
  }
  watchdog('backup_migrate', t('Database backup file deleted: %file', array('%file' => $path)));
  return user_access('access backup files') ? "admin/content/backup_migrate/files" : "admin/content/backup_migrate";
}

/**
 * The schedule form.
 */
function backup_migrate_schedule() {
  $form = array();
  $form['backup_migrate_schedule_backup_period'] = array(
    "#type" => "textfield",
    "#title" => t("Backup every"),
    "#field_suffix" => t("Hour(s)"),
    "#description" => t("Use 0 for no scheduled backup. Cron must be configured to run for backups to work."),
    "#default_value" => variable_get("backup_migrate_schedule_backup_period", 0),
  );
  $form['backup_migrate_schedule_backup_keep'] = array(
    "#type" => "textfield",
    "#title" => t("Number of Backup files to keep"),
    "#description" => t("The number of backup files to keep before deleting old ones. Use 0 to never delete backups"),
    "#default_value" => variable_get("backup_migrate_schedule_backup_keep", 0),
  );

  if (!_backup_migrate_check_destination_dir('scheduled')) {
    $form['backup_migrate_schedule_backup_period']['#disabled'] = TRUE;
    $form['backup_migrate_schedule_backup_keep']['#disabled'] = TRUE;
  }

  return system_settings_form($form);
}

/**
 * The backup/export form.
 */
function backup_migrate_backup() {
  $form = array();
  $tables = _backup_migrate_get_table_names();
  $form['backup_migrate_exclude_tables'] = array(
    "#type" => "select",
    "#multiple" => TRUE,
    "#title" => t("Exclude the following tables altogether"),
    "#options" => $tables,
    "#default_value" => variable_get("backup_migrate_exclude_tables", _backup_migrate_default_exclude_tables()),
    "#description" => t("The selected tables will not be added to the backup file."),
  );
  $form['backup_migrate_nodata_tables'] = array(
    "#type" => "select",
    "#multiple" => TRUE,
    "#title" => t("Exclude the data from the following tables"),
    "#options" => $tables,
    "#default_value" => variable_get("backup_migrate_nodata_tables", _backup_migrate_default_structure_only_tables()),
    "#description" => t("The selected tables will have their structure backed up but not their contents. This is useful for excluding cache data to reduce file size."),
  );
  $form['backup_migrate_file_name'] = array(
    "#type" => "textfield",
    "#title" => t("Backup file name"),
    "#default_value" => variable_get("backup_migrate_file_name", _backup_migrate_default_file_name()),
  );

  if (module_exists('token')) {
    $form['token_help'] = array(
      '#title' => t('Replacement patterns'),
      '#type' => 'fieldset',
      '#collapsible' => TRUE,
      '#collapsed' => TRUE,
      '#description' => t('Prefer raw-text replacements for text to avoid problems with HTML entities!'),
    );
    $form['token_help']['help'] = array(
      '#value' => theme('token_help', ''),
    );
  }

  $compression_options = array("none" => t("No Compression"));
  if (@function_exists("gzencode")) {
    $compression_options['gzip'] = t("GZip");
  }
  if (@function_exists("bzcompress")) {
    $compression_options['bzip'] = t("BZip");
  }
  if (class_exists('ZipArchive')) {
    $compression_options['zip'] = t("Zip");
  }

  $form['backup_migrate_compression'] = array(
    "#type" => "radios",
    "#title" => t("Compression"),
    "#options" => $compression_options,
    "#default_value" => variable_get("backup_migrate_compression", "none"),
  );

  $destination_options = array(
    "download" => t("Download"),
  );
  if (_backup_migrate_check_destination_dir('manual')) {
    $destination_options['save'] = t("Save to Files Directory");
  }
  $form['backup_migrate_destination'] = array(
    "#type" => "radios",
    "#title" => t("Destination"),
    "#options" => $destination_options,
    "#default_value" => variable_get("backup_migrate_destination", "download"),
  );
  $form['backup_migrate_append_timestamp'] = array(
    "#type" => "checkbox",
    "#title" => t("Append a timestamp."),
    "#default_value" => variable_get("backup_migrate_append_timestamp", 1),
  );
  $form['backup_migrate_timestamp_format'] = array(
    "#type" => "textfield",
    "#title" => t("Timestamp format"),
    "#default_value" => variable_get("backup_migrate_timestamp_format", 'Y-m-d\TH-i-s'),
    "#description" => t('Should be a PHP <a href="!url">date()</a> format string.', array('!url' => 'http://www.php.net/date')),
  );
  $form['backup_migrate_save_settings'] = array(
    "#type" => "checkbox",
    "#title" => t("Save these settings."),
    "#default_value" => 1,
  );
  $form[] = array(
    '#type' => 'submit',
    '#value' => t('Backup Database'),
  );
  return $form;
}

/**
 * Submit the form. Save the values as defaults if desired and output the backup file.
 */
function backup_migrate_backup_submit($form_id, $form_values) {
  if ($form_values['backup_migrate_save_settings']) {
    variable_set("backup_migrate_exclude_tables", $form_values['backup_migrate_exclude_tables']);
    variable_set("backup_migrate_nodata_tables", $form_values['backup_migrate_nodata_tables']);
    variable_set("backup_migrate_file_name", $form_values['backup_migrate_file_name']);
    variable_set("backup_migrate_destination", $form_values['backup_migrate_destination']);
    variable_set("backup_migrate_compression", $form_values['backup_migrate_compression']);
    variable_set("backup_migrate_append_timestamp", $form_values['backup_migrate_append_timestamp']);
    variable_set("backup_migrate_timestamp_format", $form_values['backup_migrate_timestamp_format']);
  }

  _backup_migrate_dump_tables(
    $form_values['backup_migrate_file_name'],
    $form_values['backup_migrate_exclude_tables'],
    $form_values['backup_migrate_nodata_tables'],
    'sql',
    $form_values['backup_migrate_destination'],
    $form_values['backup_migrate_compression'],
    "manual",
    $form_values['backup_migrate_append_timestamp'] ? $form_values['backup_migrate_timestamp_format'] : false
    );

  return "admin/content/backup_migrate";
}

/**
 * Restore a backup file (from the server).
 */
function _backup_migrate_restore_from_server() {
  // Merge remainder of arguments from GET['q'], into relative file path.
  $args = func_get_args();
  $path = implode('/', $args);

  if ($path && _backup_migrate_path_is_in_save_dir($path)) {
    return drupal_get_form('backup_migrate_restore_confirm', $path);
  }
  drupal_goto(user_access('access backup files') ? "admin/content/backup_migrate/files" : "admin/content/backup_migrate");
}

/**
 * Ask confirmation for file restore.
 */
function backup_migrate_restore_confirm($path) {
  $form['path'] = array('#type' => 'value', '#value' => $path);
  return confirm_form($form, t('Are you sure you want to restore the database from the backup at %path?', array('%path' => $path)), 'admin/content/backup_migrate/files', t('This will delete some or all of your data and cannot be undone. <strong>Always test your backups on a non-production server!</strong>'), t('Restore'), t('Cancel'));
}

function backup_migrate_restore_confirm_submit($form_id, $form_values) {
  $path = $form_values['path'];
  if ($path && _backup_migrate_path_is_in_save_dir($path)) {
    _backup_migrate_restore_file($path);
    watchdog('backup_migrate', t('Database restored from %file', array('%file' => $path)));
  }
  return user_access('access backup files') ? "admin/content/backup_migrate/files" : "admin/content/backup_migrate";
}

/**
 * The restore/import upload form.
 */
function backup_migrate_restore() {
  $form = array();
  $form['backup_migrate_restore_upload'] = array(
    '#title' => t('Upload a Backup File'),
    '#type' => 'file',
    '#description' => t("Upload a backup file created by this version of this module. For other database backups please use another tool for import. Max file size: %size", array("%size" => format_size(file_upload_max_size()))),
  );

  $form[] = array(
    '#type' => 'markup',
    '#value' => t('<p>This will delete some or all of your data and cannot be undone. If there is a sessions table in the backup file, you and all other currently logged in users will be logged out. <strong>Always test your backups on a non-production server!</strong></p>'),
  );

  $form[] = array(
    '#type' => 'submit',
    '#value' => t('Restore Database'),
  );
  if (user_access('access backup files')) {
    $form[] = array(
      '#type' => 'markup',
      '#value' => t('<p>Or you can restore one of the files in the <a href="!url">saved backup directory.</a></p>', array("!url" => url("admin/content/backup_migrate/files"))),
    );
  }
  $form['#attributes'] = array('enctype' => 'multipart/form-data');
  return $form;
}

/**
 * The restore submit. Do the restore.
 */
function backup_migrate_restore_submit($form_id, $form_values) {
  if ($file = file_check_upload('backup_migrate_restore_upload')) {
    _backup_migrate_restore_file($file->filepath, $file->filename, true);
    watchdog('backup_migrate', t('Database restored from upload %file', array('%file' => $file->filename)));
  }

  return 'admin/content/backup_migrate/restore';
}

/**
 * Action to backup the drupal site. Requires actions.module.
 */
function action_backup_migrate_backup($op, $edit = array()) {
  switch ($op) {
    case 'do':
      _backup_migrate_backup_with_defaults();
      watchdog('action', t('Backed up database'));
      break;

    case 'metadata':
      return array(
      'description' => t('Backup the database with the default settings'),
      'type' => t('Backup and Migrate'),
      'batchable' => TRUE,
      'configurable' => FALSE,
    );

    // Return an HTML config form for the action.

    case 'form':
      return '';

    // Validate the HTML form.

    case 'validate':
      return TRUE;

    // Process the HTML form to store configuration.

    case 'submit':
      return '';
  }
}

/*
 * Implementation of hook_action_info().
 */
function backup_migrate_action_info() {
  return array(
    'backup_migrate_action_backup' => array(
      '#label' => t('Backup the database'),
      '#module' => t('Backup and Migrate'),
      '#description' => t('Backup the database with the default settings.'),
    ),
  );
}

/*
 * Action callback.
 */
function backup_migrate_action_backup() {
  _backup_migrate_backup_with_defaults();
}

/**
 * Backup the database with the default settings.
 */
function _backup_migrate_backup_with_defaults($mode = "manual") {
  _backup_migrate_dump_tables(
    variable_get("backup_migrate_file_name", _backup_migrate_default_file_name()),
    variable_get("backup_migrate_exclude_tables", _backup_migrate_default_exclude_tables()),
    variable_get("backup_migrate_nodata_tables", _backup_migrate_default_structure_only_tables()),
    'sql',
    'save',
    variable_get("backup_migrate_compression", "none"),
    $mode,
    variable_get("backup_migrate_append_timestamp", 1) ? variable_get("backup_migrate_timestamp_format", 'Y-m-d\TH-i-s') : FALSE
    );
}

/**
 * Build the database dump file. Takes a list of tables to exclude and some formatting options.
 */
function _backup_migrate_dump_tables($filename, $exclude_tables, $nodata_tables, $type = "sql", $destination = "download", $compression = "none", $mode = "manual", $append_timestamp = FALSE) {
  $file_mime = "text/plain";
  $success = FALSE;

  if ($append_timestamp) {
    $filename .= "-". date($append_timestamp);
  }
  $filename = _backup_migrate_clean_filename($filename);

  // Dump the database.
  $temp_file = _backup_migrate_temp_file();
  switch ($type) {
    case "sql":
      $success = _backup_migrate_get_dump_sql($temp_file, $exclude_tables, $nodata_tables);
      $filename .= ".sql";
      $filemime = 'text/x-sql';
      break;
  }

  // Compress the results.
  if ($success) {
    switch ($compression) {
      case "gzip":
        $temp_gz = _backup_migrate_temp_file('gz');
        if ($success = _backup_migrate_gzip_encode($temp_file, $temp_gz, 9)) {
          $temp_file = $temp_gz;
          $filename .= ".gz";
          $filemime = 'application/x-gzip';
        }
        break;

      case "bzip":
        $temp_bz = _backup_migrate_temp_file('bz');
        if ($success = _backup_migrate_bzip_encode($temp_file, $temp_bz)) {
          $temp_file = $temp_bz;
          $filename .= ".bz";
          $filemime = 'application/x-bzip';
        }
        break;

      case "zip":
        $temp_zip = _backup_migrate_temp_file('zip');
        if ($success = _backup_migrate_zip_encode($temp_file, $temp_zip, $filename)) {
          $temp_file = $temp_zip;
          $filename .= ".zip";
          $filemime = 'application/zip';
        }
        break;
    }
  }

  // Save or download the results.
  if ($success) {
    switch ($destination) {
      case "save":
        _backup_migrate_save_to_disk($temp_file, $filename, $mode);
        break;

      case "download":
        _backup_migrate_send_file_to_download($filename, $file_mime, $temp_file);
        break;
    }
  }

  // Delete any temporary files we've created.
  _backup_migrate_temp_file("", TRUE);
}

/**
 *  Force a browser download for the file.
 */
function _backup_migrate_send_file_to_download($filename, $filetype, $file_path) {
  header('Content-Type: '. $filetype);
  header('Expires: '. gmdate('D, d M Y H:i:s') .' GMT');
  header('Content-Length: '. filesize($file_path));
  header('Content-Disposition: attachment; filename="'. $filename .'"');

  // Transfer file in 1024 byte chunks to save memory usage.
  if ($fd = fopen($file_path, 'rb')) {
    while (!feof($fd)) {
      print fread($fd, 1024);
    }
    fclose($fd);
  }

  // Delete any temporary files we've created.
  _backup_migrate_temp_file("", TRUE);

  watchdog('backup_migrate', t('Database backup file'));

  module_invoke_all('exit');
  exit();
}

/**
 * Get the sql dump file. Returns a list of sql commands, one command per line.
 *  That makes it easier to import without loading the whole file into memory.
 *  The files are a little harder to read, but human-readability is not a priority
 */
function _backup_migrate_get_dump_sql($file, $exclude_tables, $nodata_tables) {
  if ($dst = fopen($file, "w")) {
    $exclude = variable_get("backup_migrate_exclude_tables", _backup_migrate_default_exclude_tables());
    $nodata = variable_get("backup_migrate_nodata_tables", _backup_migrate_default_structure_only_tables());
    fwrite($dst, _backup_migrate_get_sql_file_header());
    $alltables = _backup_migrate_get_tables();
    foreach ($alltables as $table) {
      if ($table['Name'] && !isset($exclude[$table['Name']])) {
        fwrite($dst, _backup_migrate_get_table_structure_sql($table));
        if (!in_array($table['Name'], $nodata)) {
          _backup_migrate_dump_table_data_sql_to_handle($dst, $table);
        }
      }
    }
    fwrite($dst, _backup_migrate_get_sql_file_footer());
    fclose($dst);
    return TRUE;
  }
  else {
    return FALSE;
  }
}

/**
 * Get the sql for the structure of the given table.
 */
function _backup_migrate_get_table_structure_sql($table) {
  $out = "";
  $result = db_query("SHOW CREATE TABLE `". $table['Name'] ."`");
  if ($create = db_fetch_array($result)) {
    $out .= "DROP TABLE IF EXISTS `". $table['Name'] ."`;\n";
    $out .= strtr($create['Create Table'], "\n", " ");
    if ($table['Auto_increment']) {
      $out .= " AUTO_INCREMENT=". $table['Auto_increment'];
    }
    $out .= ";\n";
  }
  return $out;
}

/**
 *  Get the sql to insert the data for a given table
 */
function _backup_migrate_dump_table_data_sql_to_handle($dst, $table) {
  /* the maximum # of values per INSERT INTO {table}'  - to many inserted values = MySQL server goes away.
    - *Do* put more than one value per insert for performance: http://dev.mysql.com/doc/refman/5.0/en/insert-speed.html
    - *Do not* put them all in one statement: phpmyadmin can split large .sql files to deal with Apache/PHP timeouts.
    Set this to around 5-50 is my guess to end disk-bound inserts. */
  $MAX_ITEMS = 30;        
  // stop adding to the insert statement after exceeding this many bytes. Size 8000 made lines 5000->900
  $CUTOFF_BYTES = 2000;   
  // our current row.  Updated to row 1 on first use.
  $i = -1;                
  // track the number of bytes we've added for this INSERT
  $bytes = 0;             

  $data = db_query("SELECT * FROM `". $table['Name'] ."`");
  while ($row = db_fetch_array($data)) {
    $items = array();
    foreach ($row as $key => $value) {
      $items[] = is_null($value) ? "null" : "'". db_escape_string($value) ."'";
    }

    // If there is data, let's make an INSERT INTO statement.  When RESTORING, we load one statement per line.  So only one ;\n at end please.
    if ($items) {
      // It's a brand new record!  Let's get busy!
      $i++;                                         
      // close previous INSERT INTO statement on every $MAX_PER_INSERT-th item
      if ($i==$MAX_ITEMS || $bytes>$CUTOFF_BYTES) { 
        fwrite($dst, ";\n");                     
        $bytes=0;
        $i=0;
      }
      // EITHER:  start a new INSERT INTO statement on every 0th item.  NOTE: RESET $size
      if ($i==0) $bytes=fwrite($dst, "INSERT INTO `". $table['Name'] ."` VALUES ");  
      // OR:      if it's the 2nd or more-th item, add a comma.
      if ($i>0) $bytes+=fwrite($dst, ",");                                           
      // write the values
      $bytes+=fwrite($dst, "(". implode(",", $items) .")");  // add what's written to $size
     }
  } // while ($row = ...)
  
  if ($i != -1) fwrite($dst, ";\n"); // close previous INSERT INTO statement 
}

/**
 * The header for the top of the sql dump file. These commands set the connection
 *  character encoding to help prevent encoding conversion issues.
 */
function _backup_migrate_get_sql_file_header() {
  return "
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE=NO_AUTO_VALUE_ON_ZERO */;

SET NAMES utf8;
";
}

/**
 * The footer of the sql dump file.
 */
function _backup_migrate_get_sql_file_footer() {
  return "

/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
";
}

/**
 * Get a list of tables in the db. Works with MySQL, Postgres not tested.
 */
function _backup_migrate_get_tables() {
  $out = "";
  // get auto_increment values and names of all tables
  $tables = db_query("show table status");
  while ($table = db_fetch_array($tables)) {
    $out[$table['Name']] = $table;
  }
  return $out;
}

/**
 * Get the list of table names.
 */
function _backup_migrate_get_table_names() {
  $out = "";
  // Get auto_increment values and names of all tables.
  $tables = db_query("show table status");
  while ($table = db_fetch_array($tables)) {
    $out[$table['Name']] = $table['Name'];
  }
  return $out;
}

/**
 * Restore from a previously backed up files. Accepts any file created by the backup function.
 */
function _backup_migrate_restore_file($filepath, $filename = "", $delete = FALSE) {
  if (!$filename) {
    $filename = $filepath;
  }

  $file_is_temp = $delete;

  $open_func    = "fopen";
  $read_func    = "fgets";
  $close_func   = "fclose";

  // figure out if the file is compressed by the file extention
  if (drupal_substr($filename, -4, 4) == ".sql") {
    // No compression.
  }
  if (drupal_substr($filename, -3, 3) == ".gz") {
    if (function_exists("gzopen")) {
      $open_func  = "gzopen";
      $read_func  = "gzgets";
      $close_func = "gzclose";
    }
    else {
      // GZip compression... not supported.
      drupal_set_message(t("This version of PHP does not support gzip comressed files. Please try using an uncompressed sql backup."), 'error');
      drupal_goto("admin/content/backup_migrate/restore");
    }
  }
  // BZip compression.
  if (drupal_substr($filename, -3, 3) == ".bz") {
    if (function_exists("bzopen")) {
      $open_func = "fopen";
      $read_func = "fgets";

      // Decompress the file to a temp file.
      $tmp = tempnam(file_directory_temp(), 'tmp_');
      if (($dst = fopen($tmp, "w")) && ($src = bzopen($filepath, "r"))) {
        while ($data = bzread($src)) {
          fwrite($dst, $data);
        }
        fclose($dst);
        bzclose($src);
        if ($delete) {
          unlink($filepath);
        }
        $filepath = $tmp;
        $delete = TRUE;
      }
      else {
        drupal_set_message(t("Unable to decompress bzip file. Please try using an uncompressed backup."), 'error');
        drupal_goto("admin/content/backup_migrate/restore");
      }
    }
    else {
      // BZip compression... not supported.
      drupal_set_message(t("This version of PHP does not support bzip compressed files. Please try using an uncompressed backup."), 'error');
      drupal_goto("admin/content/backup_migrate/restore");
    }
  }
  // Zip compression.
  if (drupal_substr($filename, -4, 4) == ".zip") {
    if (class_exists('ZipArchive')) {
      if ($filepath != $filename) {
        rename($filepath, $filepath .".zip");
        $filepath .= ".zip";
      }

      $tmp = tempnam(file_directory_temp(), 'tmp_');
      $zip = new ZipArchive;
      if (($dst = fopen($tmp, "w")) && ($src = $zip->open($filepath))) {
        if ($data = $zip->getFromIndex(0)) {
          fwrite($dst, $data);
        }
        fclose($dst);
        $zip->close();

        if ($delete) {
          unlink($filepath);
        }
        $filepath = $tmp;
        $delete = TRUE;
      }
      else {
        drupal_set_message(t("Unable to decompress zip file. Please try using an uncompressed backup."), 'error');
        drupal_goto("admin/content/backup_migrate/restore");
      }
    }
    else {
      // Zip compression... not supported.
      drupal_set_message(t("This version of PHP does not support zip comressed files. Please try using an uncompressed backup."), 'error');
      drupal_goto("admin/content/backup_migrate/restore");
    }
  }

  // Open the file (with fopen or gzopen depending on file format).
  if ($handle = @$open_func($filepath, "r")) {
    $num = 0;

    // Read one line at a time and run the query.
    while ($line = $read_func($handle)) {
      $line = trim($line);
      if ($line) {
        // Use the helper instead of the api function to avoid substitution of '{' etc.
        _db_query($line);
        $num++;
      }
    }
    // Close the file with fclose/gzclose.
    $close_func($handle);

    // Delete the file if it is temporary.
    if ($delete) {
      unlink($filepath);
    }

    $message = t("Restore complete. %num SQL commands executed.", array("%num" => $num));
    $message .= $file_is_temp ? "" : "(". l(t("Restore Again..."), "admin/content/backup_migrate/restorefile/". $filepath) .")";
    drupal_set_message($message);
  }
  else {
    drupal_set_message(t("Unable to open file %file to restore database", array("%file" => $filepath)), 'error');
  }

  // Delete any temp files we've created.
  _backup_migrate_temp_file("", TRUE);
}

/*
  Backup File Management
 */

/**
 * Return a list of backup filetypes.
 */
function _backup_migrate_filetypes() {
  return array(
    "sql" => array(
      "extension" => ".sql",
      "filemime" => "text/x-sql",
    ),
    "gzip" => array(
      "extension" => ".gz",
      "filemime" => "application/x-gzip",
    ),
    "bzip" => array(
      "extension" => ".bz",
      "filemime" => "application/x-bzip",
    ),
    "zip" => array(
      "extension" => ".zip",
      "filemime" => "application/zip",
    ),
  );
}

/**
 * Get the basic info for a backup file on the server.
 */
function _backup_migrate_file_info($path) {
  $types = _backup_migrate_filetypes();
  foreach ($types as $type) {
    $extlen = drupal_strlen($type['extension']);
    if (drupal_substr($path, -$extlen, $extlen) === $type['extension']) {
      $out              = $type;
      $out['filesize']  = filesize($path);
      $out['filename']  = basename($path);
      $out['filemtime'] = filemtime($path);
      $out['filectime'] = filectime($path);
      $out['filepath']  = $path;
      return $out;
    }
  }
  return NULL;
}

/**
 * Save the backup file to the appropriete folder on the server.
 */
function _backup_migrate_save_to_disk($temp_file, $filename, $mode = "manual") {
  if ($dir = _backup_migrate_check_destination_dir($mode)) {
    $filepath = $dir ."/". $filename;

    rename($temp_file, $filepath);

    $message = t('Database backup saved to %file. ', array('%file' => $filepath));
    watchdog('backup_migrate', $message);
    if ($mode == "manual" && user_access('perform backup')) {
      $message .= user_access("access backup files") ? "(". l(t("Download"), "system/files/". $filepath) .") " : "";
      $message .= user_access("delete backup files") ? "(". l(t("Delete..."), "admin/content/backup_migrate/delete/". $filepath) .") " : "";
      $message .= user_access("restore from backup") ? "(". l(t("Restore..."), "admin/content/backup_migrate/restorefile/". $filepath) .")" : "";
      drupal_set_message($message);
    }
  }
}

/**
 * Implementation of hook_file_download.()
 *
 * Allow users with the appropriate permissions to download backup files.
 */
function backup_migrate_file_download($path) {
  if (_backup_migrate_path_is_in_save_dir($path)) {
    if (user_access('access backup files') && $info = _backup_migrate_file_info($path)) {
      return array(
        'Content-Type: '. $info['filemime'],
        'Content-Length: '. $info['filesize'],
        'Content-Disposition: attachment; filename="'. $info['filename'] .'"',
      );
    }
    else {
      return -1;
    }
  }
  return NULL;
}

/**
 * Get a temp file. Store it in the normal save path for slightly better security
 *   in shared environments.
 */
function _backup_migrate_temp_file($extension = "", $delete_all = FALSE) {
  static $files = array();

  if ($delete_all) {
    _backup_migrate_temp_files_delete($files);
  }
  else {
    $file = tempnam(file_directory_temp(), 'backup_migrate_');
    if (!empty($extension)) {
      unlink($file);
      $file .= '.'. $extension;
    }
    $files[] = $file;
    return $file;
  }
}

/**
 * Delete all temporary files.
 */
function _backup_migrate_temp_files_delete($files) {
  foreach ($files as $file) {
    if (file_exists($file)) {
      @unlink($file);
    }
  }
}

/**
 * List the previously created backup files.
 */
function _backup_migrate_list_files($mode = "manual") {
  $files = array();
  if ($dir = _backup_migrate_check_destination_dir($mode)) {
    if ($handle = opendir($dir)) {
      while (FALSE !== ($file = readdir($handle))) {
        $filepath = $dir ."/". $file;
        if ($info = _backup_migrate_file_info($filepath)) {
          $files[$file] = array(
            $file,
            format_date($info['filemtime']),
            format_size($info['filesize']),
            l(t("Download"), "system/files/". $filepath),
            user_access('restore from backup') ? l(t("Restore"), "admin/content/backup_migrate/restorefile/". $filepath) : '',
            user_access('delete backup files') ? l(t("Delete"), "admin/content/backup_migrate/delete/". $filepath) : '',
          );
        }
      }
    }
  }
  krsort($files);

  return theme("table", array(), $files);
}

/**
 * Remove older backups keeping only the number specified by the aministrator.
 */
function _backup_migrate_remove_expired_backups() {
  $num_to_keep = variable_get("backup_migrate_schedule_backup_keep", 0);
  // If num to keep is not 0 (0 is infinity).
  if ($num_to_keep && $dir = _backup_migrate_check_destination_dir("scheduled")) {

    // Create a list of backup files indexed by time (with an aditional index to
    // prevent two files with the same create time from overwriting each other).
    $res   = opendir($dir);
    $files = array();
    $i     = 0;
    if ($res) {
      // Read all files and sort them by modified time.
      while ($file = readdir($res)) {
        $filepath = $dir ."/". $file;
        if ($info = _backup_migrate_file_info($filepath)) {
          $files[str_pad($info['filemtime'], 10, "0", STR_PAD_LEFT) ."-". $i++] = $filepath;
        }
      }
    }

    // If we are beyond our limit, remove as many as we need.
    $num_files = count($files);
    if ($num_files > $num_to_keep) {
      $num_to_delete = $num_files - $num_to_keep;
      // Sort by date.
      ksort($files);
      // Delete from the start of the list (earliest).
      for ($i = 0; $i < $num_to_delete; $i++) {
        $filepath = array_shift($files);
        file_delete($filepath);
      }
    }
  }
}

/**
 * Return the path on the server to save the dump files.
 */
function _backup_migrate_path_is_in_save_dir($path, $mode = "") {
  $backup_dir = _backup_migrate_get_save_path($mode);
  return file_exists($backup_dir) && file_check_location($path, $backup_dir);
}

/**
 * Return the path on the server to save the dump files.
 */
function _backup_migrate_get_save_path($mode = "") {
  $dir = file_directory_path() ."/backup_migrate";
  if ($mode) {
    $dir .= $mode == "manual" ? "/manual" : "/scheduled";
  }
  return $dir;
}

/**
 * Prepare the destination directory for the backups.
 */
function _backup_migrate_check_destination_dir($mode = "") {
  $directory = _backup_migrate_get_save_path();
  $out = $subdir = rtrim($directory ."/". $mode, "/");

  // Check for the main directory.
  if (!file_check_directory($directory, TRUE)) {
    // unable to create main directory
    $message = t("Unable to create or write to the save directory '%directory'. Please check the file permissions on your files directory.", array('%directory' => $directory));
    drupal_set_message($message, "error");
    return FALSE;
  }

  // Create subdir if needed.
  if (($subdir != $directory) && !file_check_directory($subdir, TRUE)) {
    // Unable to create sub directory.
    $message = t("Unable to create or write to the save directory '%directory'. Please check the file permissions on your files directory.", array('%directory' => $subdir));
    drupal_set_message($message, "error");
    return FALSE;
  }

  // Check for a htaccess file which adequately protects the backup files.
  $htaccess_lines = "order allow,deny\ndeny from all\n";
  if (!is_file($directory .'/.htaccess') || strpos(file_get_contents($directory .'/.htaccess'), $htaccess_lines) === FALSE) {
    // Attempt to protect the backup files from public access using htaccess.
    if (($fp = @fopen($directory .'/.htaccess', 'w')) && @fputs($fp, $htaccess_lines)) {
      fclose($fp);
      chmod($directory .'/.htaccess', 0664);
    }
    // Unable to create htaccess... warn the user.
    else {
      $message = "Security warning: Couldn't modify .htaccess file. Please create a .htaccess file in your %directory directory which contains the following lines: <code>!htaccess</code> or add them to the existing .htaccess file";
      $replace = array('%directory' => $directory, '!htaccess' => '<br />'. nl2br(check_plain($htaccess_lines)));
      drupal_set_message(t($message, $replace), "error");
      watchdog('security', t($message, $replace), WATCHDOG_ERROR);
      return FALSE;
    }
  }

  // Check the user agent to make sure we're not responding to a request from drupal itself.
  // That should prevent infinite loops which could be caused by poormanscron in some circumstances.
  if (strpos($_SERVER['HTTP_USER_AGENT'], 'Drupal') !== FALSE) {
    return FALSE;
  }

  // Check to see if the destination is publicly accessible
  $test_contents = "this file should not be publicly accesible";
  // Create the the text.txt file if it's not already there.
  if (!is_file($subdir .'/test.txt') || file_get_contents($subdir .'/test.txt') != $test_contents) {
    if ($fp = fopen($subdir .'/test.txt', 'w')) {
      @fputs($fp, $test_contents);
      fclose($fp);
    }
    else {
      $message = t("Security notice: Backup and Migrate was unable to write a test text file to the destination directory %directory, and is therefore unable to check the security of the backup destination. Backups to the server will be disabled until the destination becomes writable and secure.", array('%directory' => $directory));
      drupal_set_message($message, "error");
      return FALSE;
    }
  }

  // Attempt to read the test file via http. This may fail for other reasons, so it's not a bullet-proof check.
  $path = trim(substr($subdir .'/test.txt', strlen(file_directory_path())), '\\/');
  if (_backup_migrate_test_file_readable_remotely($path, $contents)) {
    $message = t("Security notice: Backup and Migrate will not save backup files to the server because the destination directory is publicly accessible. If you want to save files to the server, please secure the '%directory' directory", array('%directory' => $directory));
    drupal_set_message($message, "error");
    return FALSE;
  }

  return $out;
}

/**
 * Check if a file can be read remotely via http.
 */
function _backup_migrate_test_file_readable_remotely($path, $contents) {
  $url = $GLOBALS['base_url'] .'/'. file_directory_path() .'/'. str_replace('\\', '/', $path);
  $result = drupal_http_request($url);
  if (strpos($result->data, $contents) !== FALSE) {
    return TRUE;
  }
  return FALSE;
}

/**
 * Gzip encode a file.
 */
function _backup_migrate_gzip_encode($source, $dest, $level = 9) {
  $success = FALSE;
  if (@function_exists("gzopen")) {
    if (($fp_out = gzopen($dest, 'wb'. $level)) && ($fp_in = fopen($source, 'rb'))) {
      while (!feof($fp_in)) {
        gzwrite($fp_out, fread($fp_in, 1024 * 512));
      }
      $success = TRUE;
    }
    @fclose($fp_in);
    @gzclose($fp_out);
  }
  return $success;
}

/**
 * Bzip encode a file.
 */
function _backup_migrate_bzip_encode($source, $dest) {
  $success = FALSE;
  if (@function_exists("bzopen")) {
    if (($fp_out = bzopen($dest, 'w')) && ($fp_in = fopen($source, 'rb'))) {
      while (!feof($fp_in)) {
        bzwrite($fp_out, fread($fp_in, 1024 * 512));
      }
      $success = TRUE;
    }
    else {
      $error = TRUE;
    }
    @fclose($fp_in);
    @bzclose($fp_out);
  }
  return $success;
}

/**
 * Zip encode a file.
 */
function _backup_migrate_zip_encode($source, $dest, $filename) {
  $success = FALSE;
  if (class_exists('ZipArchive')) {
    $zip = new ZipArchive;
    $res = $zip->open($dest, constant("ZipArchive::CREATE"));
    if ($res === TRUE) {
      $zip->addFile($source, $filename);
      $success = $zip->close();
    }
  }
  return $success;
}

/*
 Defaults
 */

/**
 * Construct a default filename using the site's name.
 */
function _backup_migrate_default_file_name() {
  if (module_exists('token')) {
    return '[site-name]';
  }
  else {
    return _backup_migrate_clean_filename(variable_get('site_name', "backup_migrate"));
  }
}

/**
 * Construct a default filename using token and some cleaning.
 */
function _backup_migrate_clean_filename($filename) {
  if (module_exists('token') && function_exists('token_replace')) {
    $filename = token_replace($filename, 'global');
  }
  $filename = preg_replace("/[^a-zA-Z0-9\.\-_]/", "", $filename);
  if (strlen($filename) > 50) {
    $filename = drupal_substr($filename, 0, 50);
  }
  if (strlen($filename) == 0) {
    $filename = 'untitled';
  }
  return $filename;
}

/**
 * Tables to ingore altogether. None by default.
 */
function _backup_migrate_default_exclude_tables() {
  return array();
}

/**
 * Return the default tables whose data can be ignored. These tables mostly contain
 *  info which can be easily reproducted (such as cache or search index)
 *  but also tables which can become quite bloated but are not necessarily extremely
 *  important to back up or migrate during development (such ass access log and watchdog)
 */
function _backup_migrate_default_structure_only_tables() {
  $core = array(
    'cache',
    'cache_filter',
    'cache_calendar_ical',
    'cache_menu',
    'cache_page',
    'cache_views',
    'sessions',
    'search_dataset',
    'search_index',
    'search_keywords_log',
    'search_total',
    'watchdog',
    'accesslog',
    'devel_queries',
    'devel_times',
  );
  $alltables = array_merge($core, module_invoke_all('devel_caches'));
  global $db_prefix;
  foreach ($alltables as $table) {
    $prefixed_tables[] = $db_prefix . $table;
  }
  return $prefixed_tables;
}

