forked from extern/egroupware
1152 lines
34 KiB
PHP
1152 lines
34 KiB
PHP
<?php
|
|
/**
|
|
* EGroupware API: Database backups
|
|
*
|
|
* @link http://www.egroupware.org
|
|
* @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
|
|
* @package api
|
|
* @subpackage db
|
|
* @author Ralf Becker <RalfBecker-AT-outdoor-training.de>
|
|
* @copyright (c) 2003-15 by Ralf Becker <RalfBecker-AT-outdoor-training.de>
|
|
* @version $Id$
|
|
*/
|
|
|
|
/**
|
|
* DB independent backup and restore of EGroupware database
|
|
*
|
|
* Backing up bool columns now for all databases as 1 or 0, but understanding PostgreSQL 't' or 'f' too.
|
|
*/
|
|
class db_backup
|
|
{
|
|
/**
|
|
* Configuration table.
|
|
*/
|
|
const TABLE = 'egw_config';
|
|
/**
|
|
* Reference to schema_proc
|
|
*
|
|
* @var schema_proc
|
|
*/
|
|
var $schema_proc;
|
|
/**
|
|
* Reference to ADOdb (connection) object
|
|
*
|
|
* @var ADOConnection
|
|
*/
|
|
var $adodb;
|
|
/**
|
|
* DB schemas, as array tablename => schema
|
|
*
|
|
* @var array
|
|
*/
|
|
var $schemas = array();
|
|
/**
|
|
* Tables to exclude from the backup: sessions, diverse caches which get automatic rebuild
|
|
*
|
|
* @var array
|
|
*/
|
|
var $exclude_tables = array(
|
|
'egw_sessions','egw_app_sessions','phpgw_sessions','phpgw_app_sessions', // eGW's session-tables
|
|
'phpgw_anglemail', // email's cache
|
|
'egw_felamimail_cache','egw_felamimail_folderstatus','phpgw_felamimail_cache','phpgw_felamimail_folderstatus', // felamimail's cache
|
|
'egw_phpfreechat', // as of the fieldnames of the table a restore would fail within egroupware, and chatcontent is of no particular intrest
|
|
);
|
|
/**
|
|
* regular expression to identify system-tables => ignored for schema+backup
|
|
*
|
|
* @var string|boolean
|
|
*/
|
|
var $system_tables = false;
|
|
/**
|
|
* Regular expression to identify eGW tables => if set only they are used
|
|
*
|
|
* @var string|boolean
|
|
*/
|
|
var $egw_tables = false;
|
|
/**
|
|
* Backup directory.
|
|
*
|
|
* @var string
|
|
*/
|
|
var $backup_dir;
|
|
/**
|
|
* Minimum number of backup files to keep. Zero for: Disable cleanup.
|
|
*
|
|
* @var integer
|
|
*/
|
|
var $backup_mincount;
|
|
/**
|
|
* Backup Files config value, will be overwritten by the availability of the ZibArchive libraries
|
|
*
|
|
* @var boolean
|
|
*/
|
|
var $backup_files = false ;
|
|
/**
|
|
* Reference to schema_proc's egw_db object
|
|
*
|
|
* @var egw_db
|
|
*/
|
|
var $db;
|
|
|
|
/**
|
|
* Constructor
|
|
*/
|
|
function __construct()
|
|
{
|
|
if (isset($GLOBALS['egw_setup']) && is_object($GLOBALS['egw_setup']) && !isset($GLOBALS['egw_setup']->db))
|
|
{
|
|
$GLOBALS['egw_setup']->loaddb(); // we run inside setup, but db object is not loaded
|
|
}
|
|
if (isset($GLOBALS['egw_setup']->oProc) && is_object($GLOBALS['egw_setup']->oProc)) // schema_proc already instanciated, use it
|
|
{
|
|
$this->schema_proc = $GLOBALS['egw_setup']->oProc;
|
|
}
|
|
else
|
|
{
|
|
$this->schema_proc = new schema_proc();
|
|
}
|
|
|
|
$this->db = $this->schema_proc->m_odb;
|
|
if (!$this->db->Link_ID) $this->db->connect();
|
|
$this->adodb = $this->db->Link_ID;
|
|
if (isset($GLOBALS['egw_setup']) && is_object($GLOBALS['egw_setup'])) // called from setup
|
|
{
|
|
if ($GLOBALS['egw_setup']->config_table && $GLOBALS['egw_setup']->table_exist(array($GLOBALS['egw_setup']->config_table)))
|
|
{
|
|
$this->db->query("SELECT config_value FROM {$GLOBALS['egw_setup']->config_table} WHERE config_app='phpgwapi' AND config_name='backup_dir'",__LINE__,__FILE__);
|
|
$this->db->next_record();
|
|
if (!($this->backup_dir = $this->db->f(0)))
|
|
{
|
|
$this->db->query("SELECT config_value FROM {$GLOBALS['egw_setup']->config_table} WHERE config_app='phpgwapi' AND config_name='files_dir'",__LINE__,__FILE__);
|
|
$this->db->next_record();
|
|
$this->backup_dir = $this->db->f(0).'/db_backup';
|
|
}
|
|
$this->db->query("SELECT config_value FROM {$GLOBALS['egw_setup']->config_table} WHERE config_app='phpgwapi' AND config_name='files_dir'",__LINE__,__FILE__);
|
|
$this->db->next_record();
|
|
if (!($this->files_dir = $this->db->f(0)))
|
|
{
|
|
error_log(__METHOD__."->"."No files Directory set/found");
|
|
}
|
|
$this->db->query("SELECT config_value FROM {$GLOBALS['egw_setup']->config_table} WHERE config_app='phpgwapi' AND config_name='system_charset'",__LINE__,__FILE__);
|
|
$this->db->next_record();
|
|
$this->charset = $this->db->f(0);
|
|
if (!$this->charset)
|
|
{
|
|
$this->db->query("SELECT content FROM {$GLOBALS['egw_setup']->lang_table} WHERE message_id='charset' AND app_name='common' AND lang!='en'",__LINE__,__FILE__);
|
|
$this->db->next_record();
|
|
$this->charset = $this->db->f(0);
|
|
}
|
|
$this->db->select($GLOBALS['egw_setup']->applications_table,'app_version',array('app_name'=>'phpgwapi'),__LINE__,__FILE__);
|
|
$this->api_version = $this->db->next_record() ? $this->db->f(0) : false;
|
|
/* Backup settings */
|
|
$this->db->query("SELECT config_value FROM {$GLOBALS['egw_setup']->config_table} WHERE config_app='phpgwapi' AND config_name='backup_mincount'",__LINE__,__FILE__);
|
|
$this->db->next_record();
|
|
$this->backup_mincount = $this->db->f(0);
|
|
// backup files too
|
|
$this->db->query("SELECT config_value FROM {$GLOBALS['egw_setup']->config_table} WHERE config_app='phpgwapi' AND config_name='backup_files'",__LINE__,__FILE__);
|
|
$this->db->next_record();
|
|
$this->backup_files = (bool)$this->db->f(0);
|
|
}
|
|
if (!$this->charset) $this->charset = 'iso-8859-1';
|
|
}
|
|
else // called from eGW
|
|
{
|
|
$this->schema_proc = new schema_proc();
|
|
if (!($this->backup_dir = $GLOBALS['egw_info']['server']['backup_dir']))
|
|
{
|
|
$this->backup_dir = $GLOBALS['egw_info']['server']['files_dir'].'/db_backup';
|
|
}
|
|
$this->files_dir = $GLOBALS['egw_info']['server']['files_dir'];
|
|
$this->backup_mincount = $GLOBALS['egw_info']['server']['backup_mincount'];
|
|
$this->backup_files = $GLOBALS['egw_info']['server']['backup_files'];
|
|
$this->charset = $GLOBALS['egw']->translation->charset();
|
|
|
|
$this->api_version = $GLOBALS['egw_info']['apps']['phpgwapi']['version'];
|
|
}
|
|
// Set a default value if not set.
|
|
if (!isset($this->backup_mincount))
|
|
{
|
|
$this->backup_mincount = 0; // Disabled if not set
|
|
}
|
|
if (!isset($this->backup_files))
|
|
{
|
|
$this->backup_files = false; // Disabled if not set
|
|
}
|
|
|
|
if (!is_dir($this->backup_dir) && is_writable(dirname($this->backup_dir)))
|
|
{
|
|
mkdir($this->backup_dir);
|
|
}
|
|
switch($this->db->Type)
|
|
{
|
|
case 'sapdb':
|
|
case 'maxdb':
|
|
//$this->system_tables = '/^(sql_cursor.*|session_roles|activeconfiguration|cachestatistics|commandcachestatistics|commandstatistics|datastatistics|datavolumes|hotstandbycomponent|hotstandbygroup|instance|logvolumes|machineconfiguration|machineutilization|memoryallocatorstatistics|memoryholders|omslocks|optimizerinformation|sessions|snapshots|spinlockstatistics|version)$/i';
|
|
$this->egw_tables = '/^(egw_|phpgw_|g2_)/i';
|
|
break;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Opens the backup-file using the highest available compression
|
|
*
|
|
* @param $name =false string/boolean filename to use, or false for the default one
|
|
* @param $reading =false opening for reading ('rb') or writing ('wb')
|
|
* @return string/resource/zip error-msg of file-handle
|
|
*/
|
|
function fopen_backup($name=false,$reading=false)
|
|
{
|
|
//echo "function fopen_backup($name,$reading)<br>"; // !
|
|
if (!$name)
|
|
{
|
|
//echo '-> !$name<br>'; // !
|
|
if (!$this->backup_dir || !is_writable($this->backup_dir))
|
|
{
|
|
//echo ' -> !$this->backup_dir || !is_writable($this->backup_dir)<br>'; // !
|
|
return lang("backupdir '%1' is not writeable by the webserver",$this->backup_dir);
|
|
}
|
|
$name = $this->backup_dir.'/db_backup-'.date('YmdHi');
|
|
}
|
|
else // remove the extension, to use the correct wrapper based on the extension
|
|
{
|
|
//echo '-> else<br>'; // !
|
|
$name = preg_replace('/\.(bz2|gz)$/i','',$name);
|
|
}
|
|
$mode = $reading ? 'rb' : 'wb';
|
|
list( , $type) = explode('.', basename($name));
|
|
if($type == 'zip' && $reading && $this->backup_files)
|
|
{
|
|
//echo '-> $type == "zip" && $reading<br>'; // !
|
|
if(!class_exists('ZipArchive', false))
|
|
{
|
|
$this->backup_files = false;
|
|
//echo ' -> (new ZipArchive) == NULL<br>'; // !
|
|
return lang("Cant open %1, needs ZipArchive", $name)."<br>\n";
|
|
}
|
|
if(!($f = fopen($name, $mode)))
|
|
{
|
|
//echo ' -> !($f = fopen($name, $mode))<br>'; // !
|
|
$lang_mode = $reading ? lang("reading") : lang("writing");
|
|
return lang("Cant open '%1' for %2", $name, $lang_mode)."<br>";
|
|
}
|
|
return $f;
|
|
}
|
|
if(class_exists('ZipArchive', false) && !$reading && $this->backup_files)
|
|
{
|
|
//echo '-> (new ZipArchive) != NULL && !$reading; '.$name.'<br>'; // !
|
|
if(!($f = fopen($name, $mode)))
|
|
{
|
|
//echo ' -> !($f = fopen($name, $mode))<br>'; // !
|
|
$lang_mode = $reading ? lang("reading") : lang("writing");
|
|
return lang("Cant open '%1' for %2", $name, $lang_mode)."<br>";
|
|
}
|
|
return $f;
|
|
}
|
|
if(!($f = fopen("compress.bzip2://$name.bz2", $mode)) &&
|
|
!($f = fopen("compress.zlib://$name.gz",$mode)) &&
|
|
!($f = fopen($name,$mode))
|
|
)
|
|
{
|
|
//echo '-> !($f = fopen("compress.bzip2://$name.bz2", $mode))<br>'; // !
|
|
$lang_mode = $reading ? lang("reading") : lang("writing");
|
|
return lang("Cant open '%1' for %2", $name, $lang_mode)."<br>";
|
|
}
|
|
return $f;
|
|
}
|
|
|
|
/**
|
|
* Remove old backups, leaving at least
|
|
* backup_mincount backup files in place. Only backup files with
|
|
* the regular name scheme are taken into account.
|
|
*
|
|
* @param files_return Fills a given array of file names to display (if given).
|
|
*/
|
|
function housekeeping(&$files_return = false)
|
|
{
|
|
/* Stop housekeeping in case it is disabled. */
|
|
if ($this->backup_mincount == 0)
|
|
{
|
|
return;
|
|
}
|
|
/* Search the backup directory for matching files. */
|
|
$handle = @opendir($this->backup_dir);
|
|
$files = array();
|
|
while($handle && ($file = readdir($handle)))
|
|
{
|
|
/* Filter for only the files with the regular name (un-renamed).
|
|
* Leave special backup files (renamed) in place.
|
|
* Note that this also excludes "." and "..".
|
|
*/
|
|
if (preg_match("/^db_backup-[0-9]{12}(\.bz2|\.gz|\.zip|)$/",$file))
|
|
{
|
|
$files[filectime($this->backup_dir.'/'.$file)] = $file;
|
|
}
|
|
}
|
|
if ($handle) closedir($handle);
|
|
|
|
/* Sort the files by ctime. */
|
|
krsort($files);
|
|
$count = 0;
|
|
foreach($files as $file)
|
|
{
|
|
if ($count >= $this->backup_mincount)//
|
|
{
|
|
$ret = unlink($this->backup_dir.'/'.$file);
|
|
if (($ret) && (is_array($files_return)))
|
|
{
|
|
array_push($files_return, $file);
|
|
}
|
|
}
|
|
$count ++;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Save the housekeeping configuration in the database and update the local variables.
|
|
*
|
|
* @param int $minCount Minimum number of backups to keep.
|
|
* @param boolean $backupFiles include files in backup or not, default dont change!
|
|
*/
|
|
function saveConfig($minCount,$backupFiles=null)
|
|
{
|
|
config::save_value('backup_mincount',$this->backup_mincount=(int)$minCount,'phpgwapi');
|
|
|
|
if (!is_null($backupFiles))
|
|
{
|
|
config::save_value('backup_files',$this->backup_files=(boolean)$backupFiles,'phpgwapi');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Certain config settings NOT to restore (because they break a working system)
|
|
*
|
|
* @var array
|
|
*/
|
|
static $system_config = array(
|
|
'files_dir',
|
|
'temp_dir',
|
|
'backup_dir',
|
|
'backup_files',
|
|
'webserver_url',
|
|
'aspell_path',
|
|
'hostname',
|
|
'httpproxy_server',
|
|
'httpproxy_port',
|
|
'httpproxy_server_username',
|
|
'httpproxy_server_password',
|
|
'system_charset',
|
|
'usecookies',
|
|
'install_id', // do not restore install_id, as that would give two systems with identical install_id
|
|
);
|
|
|
|
/**
|
|
* Backup all data in the form of a (compressed) csv file
|
|
*
|
|
* @param resource $f file opened with fopen for reading
|
|
* @param boolean $convert_to_system_charset =true obsolet, it's now allways done
|
|
* @param string $filename ='' gives the file name which is used in case of a zip archive.
|
|
* @param boolean $protect_system_config =true should above system_config values be protected (NOT overwritten)
|
|
* @param int $insert_n_rows =10 how many rows to insert in one sql statement
|
|
*
|
|
* @returns An empty string or an error message in case of failure.
|
|
*/
|
|
function restore($f,$convert_to_system_charset=true,$filename='',$protect_system_config=true, $insert_n_rows=10)
|
|
{
|
|
@set_time_limit(0);
|
|
ini_set('auto_detect_line_endings',true);
|
|
|
|
if (true) $convert_to_system_charset = true; // enforce now utf-8 as system charset restores of old backups
|
|
|
|
if ($protect_system_config)
|
|
{
|
|
$system_config = array();
|
|
foreach($this->db->select(self::TABLE,'*',array(
|
|
'config_app' => 'phpgwapi',
|
|
'config_name' => self::$system_config,
|
|
),__LINE__,__FILE__) as $row)
|
|
{
|
|
$system_config[] = $row;
|
|
}
|
|
}
|
|
if (substr($this->db->Type,0,5) != 'mysql') $this->db->transaction_begin();
|
|
|
|
// drop all existing tables
|
|
foreach($this->adodb->MetaTables('TABLES') as $table)
|
|
{
|
|
if ($this->system_tables && preg_match($this->system_tables,$table) ||
|
|
$this->egw_tables && !preg_match($this->egw_tables,$table))
|
|
{
|
|
continue;
|
|
}
|
|
$this->schema_proc->DropTable($table);
|
|
}
|
|
// it could be an old backup
|
|
list( , $type) = explode('.', basename($filename));
|
|
$dir = $this->files_dir; // $GLOBALS['egw_info']['server']['files_dir'];
|
|
// we may have to clean up old backup - left overs
|
|
if (is_dir($dir.'/database_backup'))
|
|
{
|
|
self::remove_dir_content($dir.'/database_backup/');
|
|
rmdir($dir.'/database_backup');
|
|
}
|
|
|
|
$list = array();
|
|
$name = "";
|
|
$zip = NULL;
|
|
$_f = NULL;
|
|
if($type == 'zip')
|
|
{
|
|
// has already been verified to be available in fopen_backup
|
|
$zip = new ZipArchive;
|
|
if(($zip->open($filename)) !== TRUE)
|
|
{
|
|
return lang("Cant open '%1' for %2", $filename, lang("reading"))."<br>\n";
|
|
}
|
|
self::remove_dir_content($dir); // removes the files-dir
|
|
$zip->extractTo($dir);
|
|
$_f = $f;
|
|
$list = $this->get_file_list($dir.'/database_backup/');
|
|
$name = $dir.'/database_backup/'.basename($list[0]);
|
|
if(!($f = fopen($name, 'rb')))
|
|
{
|
|
return lang("Cant open '%1' for %2", $filename, lang("reading"))."<br>\n";
|
|
}
|
|
}
|
|
// do not stop if for whatever reason some sql statement fails
|
|
if ($this->db->Halt_On_Error != 'no')
|
|
{
|
|
$backup_db_halt_on_error = $this->db->Halt_On_Error;
|
|
$this->db->Halt_On_Error = 'no';
|
|
}
|
|
$this->db_restore($f, $insert_n_rows);
|
|
|
|
if ($convert_to_system_charset) // store the changed charset
|
|
{
|
|
$this->db->insert(config::TABLE, array(
|
|
'config_value' => $this->schema_proc->system_charset,
|
|
),array(
|
|
'config_app' => 'phpgwapi',
|
|
'config_name' => 'system_charset',
|
|
),__LINE__,__FILE__);
|
|
}
|
|
// restore protected system config
|
|
if ($protect_system_config)
|
|
{
|
|
foreach($system_config as $row)
|
|
{
|
|
$this->db->insert(self::TABLE,array('config_value'=>$row['config_value']),array(
|
|
'config_name' => $row['config_name'],
|
|
'config_app' => $row['config_app'],
|
|
),__LINE__,__FILE__);
|
|
}
|
|
// check and reset cookie configuration, if it does not match current enviroment
|
|
// if $_SERVER[HTTP_HOST] does not end with cookiedomain --> delete cookiedomain
|
|
if (($cookiedomain = $this->db->select(self::TABLE,'config_value',array(
|
|
'config_app' => 'phpgwapi',
|
|
'config_name' => 'cookiedomain',
|
|
),__LINE__,__FILE__)->fetchColumn()) && isset($_SERVER['HTTP_HOST']) &&
|
|
(list($hostname) = explode(':',$_SERVER['HTTP_HOST'])) &&
|
|
substr($hostname,-strlen($cookiedomain) !== $cookiedomain))
|
|
{
|
|
$this->db->delete(self::TABLE,array(
|
|
'config_app' => 'phpgwapi',
|
|
'config_name' => 'cookiedomain',
|
|
),__LINE__,__FILE__);
|
|
}
|
|
// if configured webserver_url does NOT start with cookiepath --> delete cookiepath
|
|
if (($cookiepath = $this->db->select(self::TABLE,'config_value',array(
|
|
'config_app' => 'phpgwapi',
|
|
'config_name' => 'cookiepath',
|
|
),__LINE__,__FILE__)->fetchColumn()) &&
|
|
substr(parse_url($system_config['webserver_url'], PHP_URL_PATH),0,strlen($cookiepath) !== $cookiepath))
|
|
{
|
|
$this->db->delete(self::TABLE,array(
|
|
'config_app' => 'phpgwapi',
|
|
'config_name' => 'cookiepath',
|
|
),__LINE__,__FILE__);
|
|
}
|
|
}
|
|
// restore original Halt_On_Error state (if changed)
|
|
if ($backup_db_halt_on_error)
|
|
{
|
|
$this->db->Halt_On_Error = $backup_db_halt_on_error;
|
|
}
|
|
// zip?
|
|
if($type == 'zip')
|
|
{
|
|
fclose($f);
|
|
unlink($name);
|
|
rmdir($dir.'/database_backup');
|
|
}
|
|
if (substr($this->db->Type,0,5) != 'mysql')
|
|
{
|
|
if (!$this->db->transaction_commit())
|
|
{
|
|
return lang('Restore failed');
|
|
}
|
|
}
|
|
// generate an install_id if we dont have one (it breaks egw_cache::flush() stalling the upgrade)
|
|
unset($GLOBALS['egw_info']['server']['install_id']);
|
|
if (!($GLOBALS['egw_info']['server']['install_id'] = egw_cache::get_system_config('install_id', false)))
|
|
{
|
|
$GLOBALS['egw_info']['server']['install_id'] = md5(microtime(true).$_SERVER['HTTP_HOST']);
|
|
$this->db->insert('egw_config', array(
|
|
'config_value' => $GLOBALS['egw_info']['server']['install_id'],
|
|
), array(
|
|
'config_name' => 'install_id',
|
|
'config_app' => 'phpgwapi',
|
|
), __LINE__, __FILE__);
|
|
}
|
|
// flush instance cache
|
|
egw_cache::flush(egw_cache::INSTANCE);
|
|
|
|
// search-and-register-hooks
|
|
$GLOBALS['egw']->hooks->register_all_hooks();
|
|
|
|
return '';
|
|
}
|
|
|
|
/**
|
|
* Restore data from a (compressed) csv file
|
|
*
|
|
* @param resource $f file opened with fopen for reading
|
|
* @param int|string $insert_n_rows =10 how many rows to insert in one sql statement, or string with column-name used as unique key for insert
|
|
* @returns int number of rows read from csv file
|
|
*/
|
|
function db_restore($f, $insert_n_rows=10)
|
|
{
|
|
$convert_to_system_charset = true;
|
|
$table = False;
|
|
$n = 0;
|
|
$rows = array();
|
|
while(!feof($f))
|
|
{
|
|
$line = trim(fgets($f)); ++$n;
|
|
|
|
if (empty($line)) continue;
|
|
|
|
if (substr($line,0,9) == 'version: ')
|
|
{
|
|
$api_version = trim(substr($line,9));
|
|
continue;
|
|
}
|
|
if (substr($line,0,9) == 'charset: ')
|
|
{
|
|
$charset = trim(substr($line,9));
|
|
// needed if mbstring.func_overload > 0, else eg. substr does not work with non ascii chars
|
|
$ini_default_charset = version_compare(PHP_VERSION, '5.6', '<') ? 'mbstring.internal_encoding' : 'default_charset';
|
|
@ini_set($ini_default_charset, $charset);
|
|
|
|
// check if we really need to convert the charset, as it's not perfect and can do some damage
|
|
if ($convert_to_system_charset && !strcasecmp($this->schema_proc->system_charset, $charset))
|
|
{
|
|
$convert_to_system_charset = false; // no conversation necessary
|
|
}
|
|
// set the DB's client encoding (for mysql only if api_version >= 1.0.1.019)
|
|
if ((!$convert_to_system_charset || $this->db->capabilities['client_encoding']) &&
|
|
(substr($this->db->Type,0,5) != 'mysql' || !is_object($GLOBALS['egw_setup']) ||
|
|
$api_version && !$GLOBALS['egw_setup']->alessthanb($api_version,'1.0.1.019')))
|
|
{
|
|
$this->db->Link_ID->SetCharSet($charset);
|
|
if (!$convert_to_system_charset)
|
|
{
|
|
$this->schema_proc->system_charset = $charset; // so schema_proc uses it for the creation of the tables
|
|
}
|
|
}
|
|
continue;
|
|
}
|
|
if (substr($line,0,8) == 'schema: ')
|
|
{
|
|
// create the tables in the backup set
|
|
$this->schemas = json_php_unserialize(trim(substr($line,8)));
|
|
foreach($this->schemas as $table_name => $schema)
|
|
{
|
|
// if column is longtext in current schema, convert text to longtext, in case user already updated column
|
|
foreach($schema['fd'] as $col => &$def)
|
|
{
|
|
if ($def['type'] == 'text' && $this->db->get_column_attribute($col, $table_name, true, 'type') == 'longtext')
|
|
{
|
|
$def['type'] = 'longtext';
|
|
}
|
|
}
|
|
//echo "<pre>$table_name => ".self::write_array($schema,1)."</pre>\n";
|
|
$this->schema_proc->CreateTable($table_name, $schema);
|
|
}
|
|
continue;
|
|
}
|
|
if (substr($line,0,7) == 'table: ')
|
|
{
|
|
if ($rows) // flush pending rows of last table
|
|
{
|
|
$this->insert_multiple($table, $rows, $this->schemas[$table]);
|
|
}
|
|
$rows = array();
|
|
$table = substr($line,7);
|
|
if (!isset($this->schemas[$table])) $this->schemas[$table] = $this->db->get_table_definitions(true, $table);
|
|
$auto_id = count($this->schemas[$table]['pk']) == 1 ? $this->schemas[$table]['pk'][0] : null;
|
|
|
|
$cols = self::csv_split($line=fgets($f)); ++$n;
|
|
$blobs = array();
|
|
foreach($this->schemas[$table]['fd'] as $col => $data)
|
|
{
|
|
if ($data['type'] == 'blob') $blobs[] = $col;
|
|
}
|
|
// check if we have an old PostgreSQL backup useing 't'/'f' for bool values
|
|
// --> convert them to MySQL and our new PostgreSQL format of 1/0
|
|
$bools = array();
|
|
foreach($this->schemas[$table]['fd'] as $col => $def)
|
|
{
|
|
if ($def['type'] === 'bool') $bools[] = $col;
|
|
}
|
|
if ($table == 'egw_cal_dates') error_log(__METHOD__."() $table: bools=".array2string($bools).", schema[fd]=".array2string($this->schemas[$table]['fd']));
|
|
|
|
if (feof($f)) break;
|
|
continue;
|
|
}
|
|
if ($convert_to_system_charset && !$this->db->capabilities['client_encoding'])
|
|
{
|
|
if ($GLOBALS['egw_setup'])
|
|
{
|
|
if (!is_object($GLOBALS['egw_setup']->translation->sql))
|
|
{
|
|
$GLOBALS['egw_setup']->translation->setup_translation_sql();
|
|
}
|
|
}
|
|
}
|
|
if ($table) // do we already reached the data part
|
|
{
|
|
$import = true;
|
|
$data = self::csv_split($line, $cols, $blobs, $bools);
|
|
|
|
if ($table == 'egw_async' && in_array('##last-check-run##',$data))
|
|
{
|
|
//echo '<p>'.lang("Line %1: '%2'<br><b>csv data does contain ##last-check-run## of table %3 ==> ignored</b>",$n,$line,$table)."</p>\n";
|
|
//echo 'data=<pre>'.print_r($data,true)."</pre>\n";
|
|
$import = false;
|
|
}
|
|
if (in_array($table,$this->exclude_tables))
|
|
{
|
|
echo '<p><b>'.lang("Table %1 is excluded from backup and restore. Data will not be restored.",$table)."</b></p>\n";
|
|
$import = false; // dont restore data of excluded tables
|
|
}
|
|
if ($import)
|
|
{
|
|
if (count($data) == count($cols))
|
|
{
|
|
if ($convert_to_system_charset && !$this->db->capabilities['client_encoding'])
|
|
{
|
|
$data = translation::convert($data,$charset);
|
|
}
|
|
if ($insert_n_rows > 1)
|
|
{
|
|
$rows[] = $data;
|
|
if (count($rows) == $insert_n_rows)
|
|
{
|
|
$this->insert_multiple($table, $rows, $this->schemas[$table]);
|
|
$rows = array();
|
|
}
|
|
}
|
|
// update existing table using given unique key in $insert_n_rows (also removing auto-id/sequence)
|
|
elseif(!is_numeric($insert_n_rows))
|
|
{
|
|
$where = array($insert_n_rows => $data[$insert_n_rows]);
|
|
unset($data[$insert_n_rows]);
|
|
if ($auto_id) unset($data[$auto_id]);
|
|
$this->db->insert($table,$data,$where,__LINE__,__FILE__,false,false,$this->schemas[$table]);
|
|
}
|
|
else
|
|
{
|
|
try {
|
|
$this->db->insert($table,$data,False,__LINE__,__FILE__,false,false,$this->schemas[$table]);
|
|
}
|
|
catch(egw_exception_db_invalid_sql $e) {
|
|
echo "<p>".$e->getMessage()."</p>\n";
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
echo '<p>'.lang("Line %1: '%2'<br><b>csv data does not match column-count of table %3 ==> ignored</b>",$n,$line,$table)."</p>\n";
|
|
echo 'data=<pre>'.print_r($data,true)."</pre>\n";
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if ($rows) // flush pending rows
|
|
{
|
|
$this->insert_multiple($table, $rows, $this->schemas[$table]);
|
|
}
|
|
// updated the sequences, if the DB uses them
|
|
foreach($this->schemas as $table => $schema)
|
|
{
|
|
foreach($schema['fd'] as $column => $definition)
|
|
{
|
|
if ($definition['type'] == 'auto')
|
|
{
|
|
$this->schema_proc->UpdateSequence($table,$column);
|
|
break; // max. one per table
|
|
}
|
|
}
|
|
}
|
|
return $n;
|
|
}
|
|
|
|
/**
|
|
* Insert multiple rows ignoring doublicate entries
|
|
*
|
|
* @param string $table
|
|
* @param array $rows
|
|
* @param array $schema
|
|
*/
|
|
private function insert_multiple($table, array $rows, array $schema)
|
|
{
|
|
try {
|
|
$this->db->insert($table, $rows, False, __LINE__, __FILE__, false, false, $schema);
|
|
}
|
|
catch(egw_exception_db_invalid_sql $e)
|
|
{
|
|
// try inserting them one by one, ignoring doublicates
|
|
foreach($rows as $data)
|
|
{
|
|
try {
|
|
$this->db->insert($table, $data, False, __LINE__, __FILE__, false, false, $schema);
|
|
}
|
|
catch(egw_exception_db_invalid_sql $e) {
|
|
echo "<p>".$e->getMessage()."</p>\n";
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Removes a dir, no matter whether it is empty or full
|
|
*
|
|
* @param strin $dir
|
|
*/
|
|
private static function remove_dir_content($dir)
|
|
{
|
|
$list = scandir($dir);
|
|
while($file = $list[0])
|
|
{
|
|
if(is_dir($file) && $file != '.' && $file != '..')
|
|
self::remove_dir_content($dir.'/'.$file);
|
|
if(is_file($file) && $file != '.' && $file != '..')
|
|
unlink($dir.'/'.$file);
|
|
array_shift($list);
|
|
}
|
|
//rmdir($dir); // dont remove own dir
|
|
}
|
|
|
|
/**
|
|
* temp. replaces backslashes
|
|
*/
|
|
const BACKSLASH_TOKEN = '##!!**bAcKsLaSh**!!##';
|
|
/**
|
|
* temp. replaces NULL
|
|
*/
|
|
const NULL_TOKEN = '##!!**NuLl**!!##';
|
|
|
|
/**
|
|
* Split one line of a csv file into an array and does all unescaping
|
|
*
|
|
* @param string $line line to split
|
|
* @param array $keys =null keys to use or null to use numeric ones
|
|
* @param array $blobs =array() blob columns
|
|
* @param array $bools =array() bool columns, values might be 't'/'f' for old PostgreSQL backups
|
|
* @return array
|
|
*/
|
|
public static function csv_split($line, $keys=null, $blobs=array(), $bools=array())
|
|
{
|
|
if (function_exists('str_getcsv')) // php5.3+
|
|
{
|
|
// we need to take care of literal "NULL" values, replacing them we a special token as str_getcsv removes enclosures around strings
|
|
// str_getcsv uses '""' for '"' instead of '\\"' and does not unescape '\\n', '\\r' or '\\\\' (two backslashes)
|
|
$fields = str_getcsv(strtr($line, array(
|
|
'"NULL"' => self::NULL_TOKEN,
|
|
'\\\\' => self::BACKSLASH_TOKEN,
|
|
'\\"' => '""',
|
|
'\\n' => "\n",
|
|
'\\r' => "\r")), ',', '"', '\0');
|
|
// replace NULL-token again with 'NULL', 'NULL' with null and BACKSLASH-token with a backslash
|
|
foreach($fields as &$field)
|
|
{
|
|
switch($field)
|
|
{
|
|
case self::NULL_TOKEN:
|
|
$field = 'NULL';
|
|
break;
|
|
case 'NULL':
|
|
$field = null;
|
|
break;
|
|
default:
|
|
$field = str_replace(self::BACKSLASH_TOKEN, '\\', $field);
|
|
break;
|
|
}
|
|
}
|
|
if ($keys) // if string keys are to be used --> combine keys and values
|
|
{
|
|
$fields = array_combine($keys, $fields);
|
|
// base64-decode blob columns, if they are base64 encoded
|
|
foreach($blobs as $key)
|
|
{
|
|
if (!is_null($fields[$key]) && ($tmp = base64_decode($fields[$key], true)) !== false)
|
|
{
|
|
$fields[$key] = $tmp;
|
|
}
|
|
}
|
|
// decode bool columns, they might be 't'/'f' for old PostgreSQL backups
|
|
foreach($bools as $key)
|
|
{
|
|
$fields[$key] = egw_db::from_bool($fields[$key]);
|
|
}
|
|
}
|
|
return $fields;
|
|
}
|
|
// pre 5.3 implementation
|
|
$fields = explode(',',trim($line));
|
|
|
|
$str_pending = False;
|
|
$n = 0;
|
|
foreach($fields as $field)
|
|
{
|
|
if ($str_pending !== False)
|
|
{
|
|
$field = $str_pending.','.$field;
|
|
$str_pending = False;
|
|
}
|
|
$key = $keys ? $keys[$n] : $n;
|
|
|
|
if ($field[0] == '"')
|
|
{
|
|
if (substr($field,-1) !== '"' || $field === '"' || !preg_match('/[^\\\\]+(\\\\\\\\)*"$/',$field))
|
|
{
|
|
$str_pending = $field;
|
|
continue;
|
|
}
|
|
$arr[$key] = str_replace(self::BACKSLASH_TOKEN,'\\',str_replace(array('\\\\','\\n','\\r','\\"'),array(self::BACKSLASH_TOKEN,"\n","\r",'"'),substr($field,1,-1)));
|
|
}
|
|
elseif ($keys && strlen($field) > 26)
|
|
{
|
|
$arr[$key] = base64_decode($field);
|
|
}
|
|
elseif (in_array($key, $bools))
|
|
{
|
|
$arr[$key] = egw_db::from_bool($field);
|
|
}
|
|
else
|
|
{
|
|
$arr[$key] = $field == 'NULL' ? NULL : $field;
|
|
}
|
|
++$n;
|
|
}
|
|
return $arr;
|
|
}
|
|
|
|
/**
|
|
* escape data for csv
|
|
*/
|
|
public static function escape_data(&$data,$col,$defs)
|
|
{
|
|
if (is_null($data))
|
|
{
|
|
$data = 'NULL';
|
|
}
|
|
else
|
|
{
|
|
switch($defs[$col]['type'])
|
|
{
|
|
case 'int':
|
|
case 'auto':
|
|
case 'decimal':
|
|
case 'date':
|
|
case 'timestamp':
|
|
break;
|
|
case 'blob':
|
|
$data = base64_encode($data);
|
|
break;
|
|
case 'bool': // we use MySQL 0, 1 in csv, not PostgreSQL 't', 'f'
|
|
$data = (int)egw_db::from_bool($data);
|
|
break;
|
|
default:
|
|
$data = '"'.str_replace(array('\\',"\n","\r",'"'),array('\\\\','\\n','\\r','\\"'),$data).'"';
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Number of rows to select per chunk, to not run into memory limit on huge tables
|
|
*/
|
|
const ROW_CHUNK = 10000;
|
|
|
|
/**
|
|
* Backup all data in the form of a (compressed) csv file
|
|
*
|
|
* @param f resource file opened with fopen for writing
|
|
* @todo use https://github.com/maennchen/ZipStream-PHP to not assamble all files in memmory
|
|
*/
|
|
function backup($f)
|
|
{
|
|
//echo "function backup($f)<br>"; // !
|
|
@set_time_limit(0);
|
|
$dir = $this->files_dir; // $GLOBALS['egw_info']['server']['files_dir'];
|
|
// we may have to clean up old backup - left overs
|
|
if (is_dir($dir.'/database_backup'))
|
|
{
|
|
self::remove_dir_content($dir.'/database_backup/');
|
|
rmdir($dir.'/database_backup');
|
|
}
|
|
|
|
$file_list = array();
|
|
$name = $this->backup_dir.'/db_backup-'.date('YmdHi');
|
|
$filename = $name.'.zip';
|
|
$zippresent = false;
|
|
if(class_exists('ZipArchive') && $this->backup_files)
|
|
{
|
|
$zip = new ZipArchive;
|
|
if(is_object($zip))
|
|
{
|
|
$zippresent = true;
|
|
//echo '-> is_object($zip); '.$filename.'<br>'; // !
|
|
$res = $zip->open($filename, ZIPARCHIVE::CREATE);
|
|
if($res !== TRUE)
|
|
{
|
|
//echo ' -> !$res<br>'; // !
|
|
return lang("Cant open '%1' for %2", $filename, lang("writing"))."<br>\n";
|
|
}
|
|
$file_list = $this->get_file_list($dir);
|
|
}
|
|
}
|
|
fwrite($f,"EGroupware backup from ".date('Y-m-d H:i:s')."\n\n");
|
|
|
|
fwrite($f,"version: $this->api_version\n\n");
|
|
|
|
fwrite($f,"charset: $this->charset\n\n");
|
|
|
|
$this->schema_backup($f); // add the schema in a human readable form too
|
|
|
|
fwrite($f,"\nschema: ".json_encode($this->schemas)."\n");
|
|
|
|
foreach($this->schemas as $table => $schema)
|
|
{
|
|
if (in_array($table,$this->exclude_tables)) continue; // dont backup
|
|
|
|
// do we have a primary key?
|
|
// --> use it to order and limit rows, to kope with rows being added during backup
|
|
// otherwise new rows can cause rows being backed up twice and
|
|
// backups don't restore because of doublicate keys
|
|
$pk = $schema['pk'] && count($schema['pk']) == 1 ? $schema['pk'][0] : null;
|
|
|
|
$total = $max = 0;
|
|
do {
|
|
$num_rows = 0;
|
|
// querying only chunks for 10000 rows, to not run into memory limit on huge tables
|
|
foreach($this->db->select($table, '*',
|
|
// limit by maximum primary key already received
|
|
empty($pk) || !$max ? false : $pk.' > '.$this->db->quote($max, $schema['fd'][$pk]['type']),
|
|
__LINE__, __FILE__,
|
|
empty($pk) ? false : 0, // if no primary key, query all rows
|
|
empty($pk) ? '' : 'ORDER BY '.$this->db->name_quote($pk).' ASC', // order by primary key
|
|
false, self::ROW_CHUNK) as $row)
|
|
{
|
|
if (!empty($pk)) $max = $row[$pk];
|
|
if ($total === 0) fwrite($f,"\ntable: $table\n".implode(',',array_keys($row))."\n");
|
|
|
|
array_walk($row,array('db_backup','escape_data'),$schema['fd']);
|
|
fwrite($f,implode(',',$row)."\n");
|
|
++$total;
|
|
++$num_rows;
|
|
}
|
|
}
|
|
while(!empty($pk) && !($total % self::ROW_CHUNK) && $num_rows);
|
|
}
|
|
if(!$zippresent) // save without files
|
|
{
|
|
if ($this->backup_files)
|
|
{
|
|
echo '<center>'.lang("Cant open %1, needs ZipArchive", $name)."<br>\n".'</center>';
|
|
}
|
|
|
|
fclose($f);
|
|
if (file_exists($name)) unlink($name);
|
|
return TRUE;
|
|
}
|
|
// save files ....
|
|
//echo $name.'<br>';
|
|
$zip->addFile($name, 'database_backup/'.basename($name));
|
|
$count = 1;
|
|
foreach($file_list as $file)
|
|
{
|
|
//echo substr($file,strlen($dir)+1).'<br>';
|
|
//echo $file.'<br>';
|
|
$zip->addFile($file,substr($file,strlen($dir)+1));//,substr($file);
|
|
if(($count++) == 100) { // the file descriptor limit
|
|
$zip->close();
|
|
if(($zip = new ZipArchive())) {
|
|
$zip->open($filename);
|
|
$count =0;
|
|
}
|
|
}
|
|
}
|
|
$zip->close();
|
|
fclose($f);
|
|
unlink($name);
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* gets a list of all files on $f
|
|
*
|
|
* @param string file $f
|
|
* @param int $cnt =0
|
|
* @param string $path_name =''
|
|
*
|
|
* @return array (list of files)
|
|
*/
|
|
function get_file_list($f, $cnt = 0, $path_name = '')
|
|
{
|
|
//chdir($f);
|
|
//echo "Processing $f <br>";
|
|
if ($path_name =='') $path_name = $f;
|
|
$tlist = scandir($f);
|
|
$list = array();
|
|
$i = $cnt;
|
|
while($file = $tlist[0]) // remove all '.' and '..' and transfer to $list
|
|
{
|
|
if($file == '.' || $file == '..')
|
|
{
|
|
array_shift($tlist);
|
|
}
|
|
elseif ($file == 'debug.txt' && stripos($f,'activesync')!==false)
|
|
{
|
|
// skip activesync debug.txt on backupFiles
|
|
//error_log(__METHOD__.__LINE__.'->'.$f.'/'.$file);
|
|
array_shift($tlist);
|
|
}
|
|
else
|
|
{
|
|
if(is_dir($f.'/'.$file))
|
|
{
|
|
$nlist = $this->get_file_list($f.'/'.$file, $i);
|
|
$list += $nlist;
|
|
$i += count($nlist);
|
|
array_shift($tlist);
|
|
}
|
|
else
|
|
{
|
|
$list[$i++] = $path_name.'/'.array_shift($tlist);
|
|
}
|
|
}
|
|
}
|
|
return $list;
|
|
}
|
|
|
|
/**
|
|
* Backup all schemas in the form of a setup/tables_current.inc.php file
|
|
*
|
|
* @param resource|boolean $f
|
|
*/
|
|
function schema_backup($f=False)
|
|
{
|
|
foreach($this->adodb->MetaTables('TABLES') as $table)
|
|
{
|
|
if ($this->system_tables && preg_match($this->system_tables,$table) ||
|
|
$this->egw_tables && !preg_match($this->egw_tables,$table))
|
|
{
|
|
continue;
|
|
}
|
|
if ($this->db->Type == 'sapdb' || $this->db->Type == 'maxdb')
|
|
{
|
|
$table = strtolower($table);
|
|
}
|
|
if (!($this->schemas[$table] = $this->schema_proc->GetTableDefinition($table)))
|
|
{
|
|
unset($this->schemas[$table]);
|
|
}
|
|
if (($this->db->Type == 'sapdb' || $this->db->Type == 'maxdb') && $table == 'phpgw_anglemail')
|
|
{
|
|
// sapdb does not differ between text and blob
|
|
$this->schemas[$table]['fd']['content']['type'] = 'blob';
|
|
}
|
|
}
|
|
$def = "\t\$phpgw_baseline = ";
|
|
$def .= self::write_array($this->schemas,1);
|
|
$def .= ";\n";
|
|
|
|
if ($f)
|
|
{
|
|
fwrite($f,$def);
|
|
}
|
|
else
|
|
{
|
|
$def = "<?php\n\t/* EGroupware schema-backup from ".date('Y-m-d H:i:s')." */\n\n".$def;
|
|
html::content_header('schema-backup-'.date('YmdHi').'.inc.php','text/plain',bytes($def));
|
|
echo $def;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Dump an array as php source
|
|
*
|
|
* copied from etemplate/inc/class.db_tools.inc.php
|
|
*/
|
|
private static function write_array($arr,$depth,$parent='')
|
|
{
|
|
if (in_array($parent,array('pk','fk','ix','uc')))
|
|
{
|
|
$depth = 0;
|
|
}
|
|
if ($depth)
|
|
{
|
|
$tabs = "\n";
|
|
for ($n = 0; $n < $depth; ++$n)
|
|
{
|
|
$tabs .= "\t";
|
|
}
|
|
++$depth;
|
|
}
|
|
$def = "array($tabs".($tabs ? "\t" : '');
|
|
|
|
$n = 0;
|
|
foreach($arr as $key => $val)
|
|
{
|
|
if (!is_int($key))
|
|
{
|
|
$def .= "'$key' => ";
|
|
}
|
|
if (is_array($val))
|
|
{
|
|
$def .= self::write_array($val,$parent == 'fd' ? 0 : $depth,$key);
|
|
}
|
|
else
|
|
{
|
|
if ($key === 'nullable')
|
|
{
|
|
$def .= $val ? 'True' : 'False';
|
|
}
|
|
else
|
|
{
|
|
$def .= "'$val'";
|
|
}
|
|
}
|
|
if ($n < count($arr)-1)
|
|
{
|
|
$def .= ",$tabs".($tabs ? "\t" : '');
|
|
}
|
|
++$n;
|
|
}
|
|
$def .= "$tabs)";
|
|
|
|
return $def;
|
|
}
|
|
}
|
|
|
|
/*
|
|
$line = '"de","NULL","ranking",NULL,NULL,"one backslash: \\\\ here","\\\\","use \\"yes\\", or \\"no, prefession\\"","benützen Sie \\"yes\\" oder \\"no, Beruf\\"",NULL';
|
|
|
|
echo "<p>line='$line'</p>\n";
|
|
$fields = db_backup::csv_split($line);
|
|
echo "<pre>".print_r($fields,true)."</pre>\n";
|
|
//echo count($fields)." fields\n";
|
|
*/
|