Go to most recent revision | Blame | Compare with Previous | Last modification | View Log | Download
<?php namespace ProcessWire;/*** #pw-summary ProcessWire Database Backup and Restore* #pw-summary-initialization It’s not typically necessary to call these initialization methods unless doing manual initialization.* #pw-var $backup* #pw-instantiate $backup = $database->backups();* #pw-order-groups actions,reporting,initialization,advanced* #pw-body =* This class intentionally does not have any external dependencies (other than PDO)* so that it can be included by outside tools for restoring/exporting, with the main* example of that being the ProcessWire installer.** The recommended way to access these backup methods is via the `$database` API variable* method `$database->backups()`, which returns a `WireDatabaseBackup` instance, however* you can also initialize the class manually if you prefer, like this:* ~~~~~* // determine where backups will go (should NOT be web accessible)* $backupPath = $config->paths->assets . 'backups/';** // create a new WireDatabaseBackup instance* $backup = new WireDatabaseBackup($backupPath);** // Option 1: set the already-connected DB connection* $backup->setDatabase($this->database);** // Option 2: OR provide a Config object that contains the DB connection info* $backup->setDatabaseConfig($this->config);** ~~~~~* ### Backup the database* ~~~~~* $file = $backup->backup();* if($file) {* echo "Backed up to: $file";* } else {* echo "Backup failed: " . implode("<br>", $backup->errors());* }* ~~~~~** ### Restore a database* ~~~~~* $success = $backup->restore($file);* if($success) {* echo "Restored database from file: $file";* } else {* echo "Restore failed: " . implode("<br>", $backup->errors());* }* ~~~~~* #pw-body** ProcessWire 3.x, Copyright 2016 by Ryan Cramer* https://processwire.com***/class WireDatabaseBackup {const fileHeader = '--- WireDatabaseBackup';const fileFooter = '--- /WireDatabaseBackup';/*** ProcessWire instance, when applicable** @var ProcessWire**/protected $wire = null;/*** Options available for the $options argument to backup() method** @var array**/protected $backupOptions = array(// filename for backup: default is to make a dated filename, but this can also be used (basename only, no path)'filename' => '',// optional description of this backup'description' => '',// if specified, export will only include these tables'tables' => array(),// username to associate with the backup file (string), optional'user' => '',// exclude creating or inserting into these tables'excludeTables' => array(),// exclude creating these tables, but still export data (not supported by mysqldump)'excludeCreateTables' => array(),// exclude exporting data, but still create tables (not supported by mysqldump)'excludeExportTables' => array(),// SQL conditions for export of individual tables (table => array(SQL conditions))// The 'table' portion (index) may also be a full PCRE regexp, must start with '/' to be recognized as regex'whereSQL' => array(),// max number of seconds allowed for execution'maxSeconds' => 1200,// use DROP TABLES statements before CREATE TABLE statements?'allowDrop' => true,// use UPDATE ON DUPLICATE KEY so that INSERT statements can UPDATE when rows already present (all tables)'allowUpdate' => false,// table names that will use UPDATE ON DUPLICATE KEY (does NOT require allowUpdate=true)'allowUpdateTables' => array(),// find and replace in row data during backup (not supported by exec/mysql method)'findReplace' => array(// Example: 'databass' => 'database'),// find and replace in create table statements (not supported by exec/mysqldump)'findReplaceCreateTable' => array(// Example: 'DEFAULT CHARSET=latin1;' => 'DEFAULT CHARSET=utf8;',),// additional SQL queries to append at the bottom'extraSQL' => array(// Example: UPDATE pages SET CREATED=NOW),// EXEC MODE IS CURRRENTLY EXPERIMENTAL AND NOT RECOMMEND FOR USE YET// if true, we will try to use mysqldump (exec) first. if false, we won't attempt mysqldump.'exec' => false,// exec command to use for mysqldump (when in use)'execCommand' => '[dbPath]mysqldump--complete-insert=TRUE--add-locks=FALSE--disable-keys=FALSE--extended-insert=FALSE--default-character-set=utf8--comments=FALSE--compact--skip-disable-keys--skip-add-locks--add-drop-table=TRUE--result-file=[dbFile]--port=[dbPort]-u[dbUser]-p[dbPass]-h[dbHost][dbName][tables]');/*** Options available for the $options argument to restore() method** @var array**/protected $restoreOptions = array(// table names to restore (empty=all)'tables' => array(),// allow DROP TABLE statements?'allowDrop' => true,// DROP ALL tables before restore? (requires that 'allowDrop' must also be true)'dropAll' => false,// halt execution when an error occurs?'haltOnError' => false,// max number of seconds allowed for execution'maxSeconds' => 1200,// find and replace in row data (not supported by exec/mysql method)'findReplace' => array(// Example: 'databass' => 'database'),// find and replace in create table statements (not supported by exec/mysql)'findReplaceCreateTable' => array(// Example: 'DEFAULT CHARSET=latin1;' => 'DEFAULT CHARSET=utf8;',),// EXEC MODE IS CURRRENTLY EXPERIMENTAL AND NOT RECOMMEND FOR USE YET// if true, we will try to use mysql via exec first (faster). if false, we won't attempt that.'exec' => false,// command to use for mysql exec'execCommand' => '[dbPath]mysql--port=[dbPort]-u[dbUser]-p[dbPass]-h[dbHost][dbName] < [dbFile]',);/*** @var null|\PDO**/protected $database = null;/*** @var array**/protected $databaseConfig = array('dbUser' => '','dbPass' => '', // optional (if password is blank)'dbHost' => '','dbPort' => '','dbName' => '','dbPath' => '', // optional mysql/mysqldump path on file system'dbSocket' => '','dbCharset' => 'utf8',);/*** Array of text indicating details about what methods were used (primarily for debugging)** @var array**/protected $notes = array();/*** Array of text error messages** @var array**/protected $errors = array();/*** Database files path** @var string|null**/protected $path = null;/*** Cache for getAllTables()** @var array**/protected $tables = array();/*** Cache for getAllTables()** @var array**/protected $counts = array();/*** Construct** You should follow-up the construct call with one or both of the following:** - $backups->setDatabase(PDO|WireDatabasePDO);* - $backups->setDatabaseConfig(array|object);** #pw-group-initialization** @param string $path Path where database files are stored* @throws \Exception**/public function __construct($path = '') {if(strlen($path)) $this->setPath($path);}/*** Set the current ProcessWire instance** #pw-internal** @param ProcessWire $wire**/public function setWire($wire) {if(is_object($wire) && $wire->className() == 'ProcessWire') $this->wire = $wire;}/*** Set the database configuration information** #pw-group-initialization** @param array|Config|object $config Containing these properties:* - dbUser* - dbHost* - dbPort* - dbName* - dbPass* - dbPath (optional)* - dbCharset (optional)* @return $this* @throws \Exception if missing required config settings**/public function setDatabaseConfig($config) {foreach($this->databaseConfig as $key => $_value) {if(is_object($config) && isset($config->$key)) $value = $config->$key;else if(is_array($config) && isset($config[$key])) $value = $config[$key];else $value = '';if(empty($value) && !empty($_value)) $value = $_value; // i.e. dbCharsetif($key == 'dbPath' && $value) {$value = rtrim($value, '/') . '/';if(!is_dir($value)) $value = '';}$this->databaseConfig[$key] = $value;}$missing = array();$optional = array('dbPass', 'dbPath', 'dbSocket', 'dbPort');foreach($this->databaseConfig as $key => $value) {if(empty($value) && !in_array($key, $optional)) $missing[] = $key;}if(count($missing)) {throw new \Exception("Missing required config for: " . implode(', ', $missing));}// $charset = $this->databaseConfig['dbCharset'];// $this->backupOptions['findReplaceCreateTable']['DEFAULT CHARSET=latin1;'] = "DEFAULT CHARSET=$charset;";return $this;}/*** Set the PDO database connection** #pw-group-initialization** @param \PDO|WireDatabasePDO $database* @throws \PDOException on invalid connection**/public function setDatabase($database) {$query = $database->prepare('SELECT DATABASE()');$query->execute();list($dbName) = $query->fetch(\PDO::FETCH_NUM);if($dbName) $this->databaseConfig['dbName'] = $dbName;$this->database = $database;}/*** Get current database connection, initiating the connection if not yet active** #pw-advanced** @return null|\PDO|WireDatabasePDO* @throws \Exception**/public function getDatabase() {if($this->database) return $this->database;$config = $this->databaseConfig;if(empty($config['dbUser'])) throw new \Exception("Please call setDatabaseConfig(config) to supply config information so we can connect.");if($config['dbSocket']) {$dsn = "mysql:unix_socket=$config[dbSocket];dbname=$config[dbName];";} else {$dsn = "mysql:dbname=$config[dbName];host=$config[dbHost]";if($config['dbPort']) $dsn .= ";port=$config[dbPort]";}$options = array(\PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES '$config[dbCharset]'",\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION);$database = new \PDO($dsn, $config['dbUser'], $config['dbPass'], $options);$this->setDatabase($database);return $database;}/*** Add an error and return last error** #pw-group-reporting** @param string $str If omitted, no error is added* @return string**/public function error($str = '') {if(strlen($str)) $this->errors[] = $str; // append error messagereturn count($this->errors) ? end($this->errors) : ''; // return last error}/*** Return all error messages that occurred** #pw-group-reporting** @param bool $reset Specify true to clear out existing errors or omit just to return error messages* @return array**/public function errors($reset = false) {$errors = $this->errors;if($reset) $this->errors = array();return $errors;}/*** Record a note** #pw-group-reporting** @param $key* @param $value**/protected function note($key, $value) {if(!empty($this->notes[$key])) $this->notes[$key] .= ", $value";else $this->notes[$key] = $value;}/*** Get all notes** #pw-group-reporting** @param bool $reset* @return array**/public function notes($reset = false) {$notes = $this->notes;if($reset) $this->notes = array();return $notes;}/*** Set path where database files are stored** #pw-group-initialization** @param string $path* @return $this* @throws \Exception if path has a problem**/public function setPath($path) {$path = $this->sanitizePath($path);if(!is_dir($path)) throw new \Exception("Path doesn't exist: $path");if(!is_writable($path)) throw new \Exception("Path isn't writable: $path");$this->path = $path;return $this;}/*** Get path where database files are stored** #pw-group-reporting** @return string**/public function getPath() {return $this->path;}/*** Return array of all backup files** To get additional info on any of them, call getFileInfo($basename) method** #pw-group-reporting** @return array of strings (basenames)**/public function getFiles() {$dir = new \DirectoryIterator($this->path);$files = array();foreach($dir as $file) {if($file->isDot() || $file->isDir()) continue;$key = $file->getMTime();while(isset($files[$key])) $key++;$files[$key] = $file->getBasename();}krsort($files); // sort by date, newest to oldestreturn array_values($files);}/*** Get information about a backup file** #pw-group-reporting** @param string $filename* @return array Returns associative array of information on success, empty array on failure**/public function getFileInfo($filename) {// all possible info (null values become integers when populated)$info = array('description' => '','valid' => false,'time' => '', // ISO-8601'mtime' => null, // timestamp'user' => '','size' => null,'basename' => '','pathname' => '','dbName' => '','tables' => array(),'excludeTables' => array(),'excludeCreateTables' => array(),'excludeExportTables' => array(),'numTables' => null,'numCreateTables' => null,'numInserts' => null,'numSeconds' => null,);$filename = $this->sanitizeFilename($filename);if(!file_exists($filename)) return array();$fp = fopen($filename, "r+");$line = fgets($fp);if(strpos($line, self::fileHeader) === 0 || strpos($line, "# " . self::fileHeader) === 0) {$pos = strpos($line, '{');if($pos !== false) {$json = substr($line, $pos);$info2 = json_decode($json, true);if(!$info2) $info2 = array();foreach($info2 as $key => $value) $info[$key] = $value;}}$bytes = strlen(self::fileFooter) + 255; // some extra bytes in case something gets added at the endfseek($fp, $bytes * -1, SEEK_END);$foot = fread($fp, $bytes);$info['valid'] = strpos($foot, self::fileFooter) !== false;fclose($fp);// footer summary$pos = strpos($foot, self::fileFooter) + strlen(self::fileFooter);if($info['valid'] && $pos !== false) {$json = substr($foot, $pos);$summary = json_decode($json, true);if(is_array($summary)) $info = array_merge($info, $summary);}$info['size'] = filesize($filename);$info['mtime'] = filemtime($filename);$info['pathname'] = $filename;$info['basename'] = basename($filename);return $info;}/*** Get array of all table names** #pw-group-reporting** @param bool $count If true, returns array indexed by name with count of records as value* @param bool $cache Allow use of cache?* @return array**/public function getAllTables($count = false, $cache = true) {if($cache) {if($count && count($this->counts)) return $this->counts;if(count($this->tables)) return $this->tables;} else {$this->tables = array();$this->counts = array();}$query = $this->database->prepare('SHOW TABLES');$query->execute();/** @noinspection PhpAssignmentInConditionInspection */while($row = $query->fetch(\PDO::FETCH_NUM)) $this->tables[$row[0]] = $row[0];$query->closeCursor();if($count) {foreach($this->tables as $table) {$query = $this->database->prepare("SELECT COUNT(*) FROM `$table`");$query->execute();$row = $query->fetch(\PDO::FETCH_NUM);$this->counts[$table] = (int) $row[0];}$query->closeCursor();return $this->counts;} else {return $this->tables;}}/////////////////////////////////////////////////////////////////////////////////////////////////////////////////*** Perform a database export/dump** #pw-group-actions** @param array $options Options to modify default behavior:* - `filename` (string): filename for backup: default is to make a dated filename, but this can also be used (basename only, no path)* - `description` (string): optional description of this backup* - `tables` (array): if specified, export will only include these tables* - `user` (string): username to associate with the backup file (string), optional* - `excludeTables` (array): exclude creating or inserting into these tables* - `excludeCreateTables` (array): exclude creating these tables, but still export data* - `excludeExportTables` (array): exclude exporting data, but still create tables* - `whereSQL` (array): SQL conditions for export of individual tables [table => [SQL conditions]]. The `table` portion (index) may also be a full PCRE regexp, must start with `/` to be recognized as regex.* - `maxSeconds` (int): max number of seconds allowed for execution (default=1200)* - `allowDrop` (bool): use DROP TABLES statements before CREATE TABLE statements? (default=true)* - `allowUpdate` (bool): use UPDATE ON DUPLICATE KEY so that INSERT statements can UPDATE when rows already present (all tables). (default=false)* - `allowUpdateTables` (array): table names that will use UPDATE ON DUPLICATE KEY (does NOT require allowUpdate=true)* - `findReplace` (array): find and replace in row data during backup. Example: ['databass' => 'database']* - `findReplaceCreateTable` (array): find and replace in create table statements* Example: ['DEFAULT CHARSET=latin1;' => 'DEFAULT CHARSET=utf8;']* - `extraSQL` (array): additional SQL queries to append at the bottom. Example: ['UPDATE pages SET created=NOW()']* @return string Full path and filename of database export file, or false on failure.* @throws \Exception on fatal error* @see WireDatabaseBackup::restore()**/public function backup(array $options = array()) {if(!$this->path) throw new \Exception("Please call setPath('/backup/files/path/') first");$this->errors(true);$options = array_merge($this->backupOptions, $options);if(empty($options['filename'])) {// generate unique filename$tail = ((count($options['tables']) || count($options['excludeTables']) || count($options['excludeExportTables'])) ? '-part' : '');$n = 0;do {$options['filename'] = $this->databaseConfig['dbName'] . '_' . date('Y-m-d_H-i-s') . $tail . ($n ? "-$n" : "") . ".sql";$n++;} while(file_exists($this->path . $options['filename']));} else {$options['filename'] = basename($options['filename']);}set_time_limit($options['maxSeconds']);$file = false;if($this->supportsExec($options)) {$file = $this->backupExec($this->path . $options['filename'], $options);$this->note('method', 'exec_mysqldump');}if(!$file) {$file = $this->backupPDO($this->path . $options['filename'], $options);$this->note('method', 'pdo');}$success = false;if($file && file_exists($file)) {if(!filesize($file)) {$this->unlink($file);} else {$success = true;}}return $success ? $file : false;}/*** Unlink file using PW if available or PHP if not** @param string $file* @return bool* @throws WireException**/protected function unlink($file) {if(!is_file($file)) return false;if($this->wire) {return $this->wire->files->unlink($file, true);} else {return unlink($file);}}/*** Set backup options** #pw-internal** @param array $options* @return $this**/public function setBackupOptions(array $options) {$this->backupOptions = array_merge($this->backupOptions, $options);return $this;}/*** Start a new backup file, adding our info header to the top** @param string $file* @param array $options* @return bool**/protected function backupStartFile($file, array $options) {$fp = fopen($file, 'w+');if(!$fp) {$this->error("Unable to write header to file: $file");return false;}$info = array('time' => date('Y-m-d H:i:s'),'user' => $options['user'],'dbName' => $this->databaseConfig['dbName'],'description' => $options['description'],'tables' => $options['tables'],'excludeTables' => $options['excludeTables'],'excludeCreateTables' => $options['excludeCreateTables'],'excludeExportTables' => $options['excludeExportTables'],);$json = json_encode($info);$json = str_replace(array("\r", "\n"), " ", $json);fwrite($fp, "# " . self::fileHeader . " $json\n");fclose($fp);if($this->wire) $this->wire->files->chmod($file);return true;}/*** End a new backup file, adding our footer to the bottom** @param string|resource $file* @param array $summary* @param array $options* @return bool**/protected function backupEndFile($file, array $summary = array(), array $options = array()) {$fp = is_resource($file) ? $file : fopen($file, 'a+');if(!$fp) {$this->error("Unable to write footer to file: $file");return false;}foreach($options['extraSQL'] as $sql) {fwrite($fp, "\n" . rtrim($sql, '; ') . ";\n");}$footer = "# " . self::fileFooter;if(count($summary)) {$json = json_encode($summary);$json = str_replace(array("\r", "\n"), " ", $json);$footer .= " $json";}fwrite($fp, "\n$footer");fclose($fp);return true;}/*** Create a mysql dump file using PDO** @param string $file Path + filename to create* @param array $options* @return string|bool Returns the created file on success or false on error**/protected function backupPDO($file, array $options = array()) {$database = $this->getDatabase();$options = array_merge($this->backupOptions, $options);if(!$this->backupStartFile($file, $options)) return false;$startTime = time();$fp = fopen($file, "a+");$tables = $this->getAllTables();$numCreateTables = 0;$numTables = 0;$numInserts = 0;$hasReplace = count($options['findReplace']);foreach($tables as $table) {if(in_array($table, $options['excludeTables'])) continue;if(count($options['tables']) && !in_array($table, $options['tables'])) continue;if(in_array($table, $options['excludeCreateTables'])) {// skip} else {if($options['allowDrop']) fwrite($fp, "\nDROP TABLE IF EXISTS `$table`;");$query = $database->prepare("SHOW CREATE TABLE `$table`");$query->execute();$row = $query->fetch(\PDO::FETCH_NUM);$createTable = $row[1];foreach($options['findReplaceCreateTable'] as $find => $replace) {$createTable = str_replace($find, $replace, $createTable);}$numCreateTables++;fwrite($fp, "\n$createTable;\n");}if(in_array($table, $options['excludeExportTables'])) continue;$numTables++;$columns = array();$query = $database->prepare("SHOW COLUMNS FROM `$table`");$query->execute();/** @noinspection PhpAssignmentInConditionInspection */while($row = $query->fetch(\PDO::FETCH_NUM)) $columns[] = $row[0];$query->closeCursor();$columnsStr = '`' . implode('`, `', $columns) . '`';$sql = "SELECT $columnsStr FROM `$table` ";$conditions = array();foreach($options['whereSQL'] as $_table => $_conditions) {if($_table === $table || ($_table[0] == '/' && preg_match($_table, $table))) $conditions = array_merge($conditions, $_conditions);}if(count($conditions)) {$sql .= "WHERE ";foreach(array_values($conditions) as $n => $condition) {if($n) $sql .= "AND ";$sql .= "($condition) ";}}$query = $database->prepare($sql);$this->executeQuery($query);/** @noinspection PhpAssignmentInConditionInspection */while($row = $query->fetch(\PDO::FETCH_NUM)) {$numInserts++;$out = "\nINSERT INTO `$table` ($columnsStr) VALUES(";foreach($row as $value) {if(is_null($value)) {$value = 'NULL';} else {if($hasReplace) foreach($options['findReplace'] as $find => $replace) {if(strpos($value, $find)) $value = str_replace($find, $replace, $value);}$value = $database->quote($value);}$out .= "$value, ";}$out = rtrim($out, ", ") . ") ";if($options['allowUpdate']) {$out .= "ON DUPLICATE KEY UPDATE ";foreach($columns as $c) $out .= "`$c`=VALUES(`$c`), ";}$out = rtrim($out, ", ") . ";";fwrite($fp, $out);}$query->closeCursor();fwrite($fp, "\n");}$summary = array('numTables' => $numTables,'numCreateTables' => $numCreateTables,'numInserts' => $numInserts,'numSeconds' => time() - $startTime,);$this->backupEndFile($fp, $summary, $options); // this does the fclosereturn file_exists($file) ? $file : false;}/*** Create a mysql dump file using exec(mysqldump)** @param string $file Path + filename to create* @param array $options* @return string|bool Returns the created file on success or false on error** @todo add backupStartFile/backupEndFile support**/protected function backupExec($file, array $options) {$cmd = $options['execCommand'];$cmd = str_replace(array("\n", "\t"), ' ', $cmd);$cmd = str_replace('[tables]', implode(' ', $options['tables']), $cmd);foreach($options['excludeTables'] as $table) {$cmd .= " --ignore-table=$table";}if(strpos($cmd, '[dbFile]')) {$cmd = str_replace('[dbFile]', $file, $cmd);} else {$cmd .= " > $file";}foreach($this->databaseConfig as $key => $value) {$cmd = str_replace("[$key]", $value, $cmd);}exec($cmd);if(file_exists($file)) {if(filesize($file) > 0) return $file;$this->unlink($file);}return false;}/////////////////////////////////////////////////////////////////////////////////////////////////////////////////*** Restore/import a MySQL database dump file** This method is designed to restore dump files created by the backup() method of this* class, however it *may* also work with dump files created from other sources like* mysqldump or PhpMyAdmin.** #pw-group-actions** @param string $filename Filename to restore, optionally including path (if no path, then path set to construct is assumed)* @param array $options Options to modify default behavior:* - `tables` (array): table names to restore (empty=all)* - `allowDrop` (bool): allow DROP TABLE statements (default=true)* - `dropAll` (bool): DROP ALL tables before restore? The allowDrop optional must also be true. (default=false)* - `haltOnError` (bool): halt execution when an error occurs? (default=false)* - `maxSeconds` (int): max number of seconds allowed for execution (default=1200)* - `findReplace` (array): find and replace in row data. Example: ['databass' => 'database']* - `findReplaceCreateTable` (array): find and replace in create table statements.* Example: ['DEFAULT CHARSET=utf8;' => 'DEFAULT CHARSET=utf8mb4;']* @return true on success, false on failure. Call the errors() method to retrieve errors.* @throws \Exception on fatal error* @see WireDatabaseBackup::backup()**/public function restore($filename, array $options = array()) {$filename = $this->sanitizeFilename($filename);if(!file_exists($filename)) throw new \Exception("Restore file does not exist: $filename");$options = array_merge($this->restoreOptions, $options);set_time_limit($options['maxSeconds']);$success = false;$this->errors(true);$this->notes(true);if($this->supportsExec($options)) {$this->note('method', 'exec_mysql');$success = $this->restoreExec($filename, $options);if(!$success) $this->error("Exec mysql failed, attempting PDO...");}if(!$success) {$this->note('method', 'pdo');$success = $this->restorePDO($filename, $options);}return $success;}/*** Set restore options** #pw-internal** @param array $options* @return $this**/public function setRestoreOptions(array $options) {$this->restoreOptions = array_merge($this->restoreOptions, $options);return $this;}/*** Import a database SQL file using PDO** @param string $filename Filename to restore (must be SQL file exported by this class)* @param array $options See $restoreOptions* @return bool true on success, false on failure. Call the errors() method to retrieve errors.**/protected function restorePDO($filename, array $options = array()) {$fp = fopen($filename, "rb");if($fp === false) {$this->error("Unable to open: $filename");return false;}$numInserts = 0;$numTables = 0;$numQueries = 0;if($options['allowDrop'] === true && $options['dropAll'] === true) {$this->dropAllTables();}$tables = array(); // selective tables to restore, optionalforeach($options['tables'] as $table) $tables[$table] = $table;if(!count($tables)) $tables = null;while(!feof($fp)) {$line = trim(fgets($fp));if(!$this->restoreUseLine($line)) continue;if(preg_match('/^(INSERT|CREATE|DROP)\s+(?:INTO|TABLE IF EXISTS|TABLE IF NOT EXISTS|TABLE)\s+`?([^\s`]+)/i', $line, $matches)) {$command = strtoupper($matches[1]);$table = $matches[2];} else {$command = '';$table = '';}if($command === 'CREATE') {if(!$options['allowDrop'] && stripos($line, 'CREATE TABLE IF NOT EXISTS') === false) {$line = str_ireplace('CREATE TABLE', 'CREATE TABLE IF NOT EXISTS', $line);}} else if($command === 'DROP') {if(!$options['allowDrop']) continue;} else if($command === 'INSERT' && $tables) {if(!isset($tables[$table])) continue; // skip tables not selected for import}while(substr($line, -1) != ';' && !feof($fp)) {// get the rest of the lines in the query (if multi-line)$_line = trim(fgets($fp));if($this->restoreUseLine($_line)) $line .= $_line;}$replacements = $command === 'CREATE' ? $options['findReplaceCreateTable'] : $options['findReplace'];if(count($replacements)) foreach($replacements as $find => $replace) {if(strpos($line, $find) === false) continue;$line = str_replace($find, $replace, $line);}try {$this->executeQuery($line, $options);if($command === 'INSERT') $numInserts++;if($command === 'CREATE') $numTables++;$numQueries++;} catch(\Exception $e) {$this->error($e->getMessage());if($options['haltOnError']) break;}}fclose($fp);$this->note('queries', $numQueries);$this->note('inserts', $numInserts);$this->note('tables', $numTables);if(count($this->errors) > 0) {$this->error(count($this->errors) . " queries generated errors ($numQueries queries and $numInserts inserts for $numTables were successful)");return false;} else {return $numQueries > 0;}}/*** Import a database SQL file using exec(mysql)** @param string $filename Filename to restore (must be SQL file exported by this class)* @param array $options See $restoreOptions* @return true on success, false on failure. Call the errors() method to retrieve errors.**/protected function restoreExec($filename, array $options = array()) {$cmd = $options['execCommand'];$cmd = str_replace(array("\n", "\t"), ' ', $cmd);$cmd = str_replace('[dbFile]', $filename, $cmd);foreach($this->databaseConfig as $key => $value) {$cmd = str_replace("[$key]", $value, $cmd);}$o = array();$r = 0;exec($cmd, $o, $r);if($r > 0) {// 0=success, 1=warning, 2=not found$this->error("mysql reported error code $r");foreach($o as $e) $this->error($e);return false;}return true;}/*** Returns true or false if a line should be used for restore** @param $line* @return bool**/protected function restoreUseLine($line) {if(empty($line) || substr($line, 0, 2) == '--' || substr($line, 0, 1) == '#') return false;return true;}/*** Restore from 2 SQL files while resolving table differences (think of it as array_merge for a DB restore)** The CREATE TABLE and INSERT statements in filename2 take precedence of those in filename1.* INSERT statements from both will be executed, with filename2 INSERTs updating those of filename1.* CREATE TABLE statements in filename1 won't be executed if they also exist in filename2.** This method assumes both files follow the SQL dump format created by this class.** #pw-advanced** @param string $filename1 Original filename* @param string $filename2 Filename that may have statements that will update/override those in filename1* @param array $options* @return bool True on success, false on fail.* @throws \Exception|WireException if $options['haltOnErrors'] == true.**/public function restoreMerge($filename1, $filename2, $options) {$options = array_merge($this->restoreOptions, $options);$creates1 = $this->findCreateTables($filename1, $options);$creates2 = $this->findCreateTables($filename2, $options);$creates = array_merge($creates1, $creates2); // CREATE TABLE statements in filename2 override those in filename1$numErrors = 0;foreach($creates as $table => $create) {if($options['allowDrop']) {if(!$this->executeQuery("DROP TABLE IF EXISTS `$table`", $options)) $numErrors++;}if(!$this->executeQuery($create, $options)) $numErrors++;}$inserts = $this->findInserts($filename1);foreach($inserts as $table => $tableInserts) {foreach($tableInserts as $insert) {if(!$this->executeQuery($insert, $options)) $numErrors++;}}// Convert line 1 to line 2:// 1. INSERT INTO `field_process` (pages_id, data) VALUES('6', '17');// 2. INSERT INTO `field_process` (pages_id, data) VALUES('6', '17') ON DUPLICATE KEY UPDATE pages_id=VALUES(pages_id), data=VALUES(data);$inserts = $this->findInserts($filename2);foreach($inserts as $table => $tableInserts) {foreach($tableInserts as $insert) {// check if table existed in both dump files, and has no duplicate update statement$regex = '/\s+ON\s+DUPLICATE\s+KEY\s+UPDATE\s+[^\'";]+;$/i';if(isset($creates1[$table]) && !preg_match($regex, $insert)) {// line doesn't already contain an ON DUPLICATE section, so we need to add it$pos1 = strpos($insert, '(') + 1;$pos2 = strpos($insert, ')') - $pos1;$fields = substr($insert, $pos1, $pos2);$insert = rtrim($insert, '; ') . " ON DUPLICATE KEY UPDATE ";foreach(explode(',', $fields) as $name) {$name = trim($name);$insert .= "$name=VALUES($name), ";}$insert = rtrim($insert, ", ") . ";";}if(!$this->executeQuery($insert, $options)) $numErrors++;}}return $numErrors === 0;}/*** Drop all tables from database** @return int Quantity of tables dropped* @throws \Exception* @since 3.0.130**/public function dropAllTables() {$database = $this->getDatabase();$tables = $this->getAllTables(false, false);$qty = 0;$database->exec("SET FOREIGN_KEY_CHECKS=0");foreach($tables as $table) {if($database->exec("DROP TABLE IF EXISTS `$table`")) $qty++;}$database->exec("SET FOREIGN_KEY_CHECKS=1");return $qty;}/////////////////////////////////////////////////////////////////////////////////////////////////////////////////*** Returns array of all create table statements, indexed by table name** @param string $filename to extract all CREATE TABLE statements from* @param string $regex Regex (PCRE) to match for statement to be returned, must stuff table name into first match* @param bool $multi Whether there can be multiple matches per table* @return array of statements, indexed by table name. If $multi is true, it will be array of arrays.* @throws \Exception if unable to open specified file**/protected function findStatements($filename, $regex, $multi = true) {$filename = $this->sanitizeFilename($filename);$fp = fopen($filename, 'r');if(!$fp) throw new \Exception("Unable to open: $filename");$statements = array();while(!feof($fp)) {$line = trim(fgets($fp));if(!preg_match($regex, $line, $matches)) continue;if(empty($matches[1])) continue;$table = $matches[1];while(substr($line, -1) != ';' && !feof($fp)) $line .= " " . rtrim(fgets($fp));if($multi) {if(!isset($statements[$table])) $statements[$table] = array();$statements[$table][] = $line;} else {$statements[$table] = $line;}}fclose($fp);return $statements;}/*** Returns array of all create table statements, indexed by table name** #pw-internal** @param string $filename to extract all CREATE TABLE statements from* @param array $options* @return bool|array of CREATE TABLE statements, associative: indexed by table name* @throws \Exception if unable to open specified file**/public function findCreateTables($filename, array $options) {$regex = '/^CREATE\s+TABLE\s+`?([^`\s]+)/i';$statements = $this->findStatements($filename, $regex, false);if(!empty($options['findReplaceCreateTable'])) {foreach($options['findReplaceCreateTable'] as $find => $replace) {foreach($statements as $key => $line) {if(strpos($line, $find) === false) continue;$line = str_replace($find, $replace, $line);$statements[$key] = $line;}}}return $statements;}/*** Returns array of all INSERT statements in given filename, indexed by table name** #pw-internal** @param string $filename to extract all CREATE TABLE statements from* @return array of arrays of INSERT statements. Base array is associative indexed by table name.* Inside arrays are numerically indexed by order of appearance.**/public function findInserts($filename) {$regex = '/^INSERT\s+INTO\s+`?([^`\s]+)/i';return $this->findStatements($filename, $regex, true);}/*** Execute an SQL query, either a string or PDOStatement** @param string|\PDOStatement $query* @param bool|array $options May be boolean (for haltOnError), or array containing the property (i.e. $options array)* - `haltOnError` (bool): Halt execution when error occurs? (default=false)* @return bool Query result* @throws \Exception if haltOnError, otherwise it populates $this->errors**/protected function executeQuery($query, $options = array()) {$defaults = array('haltOnError' => false);if(is_bool($options)) {$defaults['haltOnError'] = $options;$options = array();}$options = array_merge($defaults, $options);$result = false;try {if(is_string($query)) {$result = $this->getDatabase()->exec($query);} else if($query instanceof \PDOStatement) {$result = $query->execute();}} catch(\Exception $e) {if(empty($options['haltOnError'])) {$this->error($e->getMessage());} else {throw $e;}}return $result === false ? false : true;}/*** For path: Normalizes slashes and ensures it ends with a slash** @param $path* @return string**/protected function sanitizePath($path) {if(DIRECTORY_SEPARATOR != '/') $path = str_replace(DIRECTORY_SEPARATOR, '/', $path);$path = rtrim($path, '/') . '/'; // ensure it ends with trailing slashreturn $path;}/*** For filename: Normalizes slashes and ensures it starts with a path** @param $filename* @return string* @throws \Exception if path has not yet been set**/protected function sanitizeFilename($filename) {if(DIRECTORY_SEPARATOR != '/') $filename = str_replace(DIRECTORY_SEPARATOR, '/', $filename);if(strpos($filename, '/') === false) {$filename = $this->path . $filename;}if(strpos($filename, '/') === false) {$path = $this->getPath();if(!strlen($path)) throw new \Exception("Please supply full path to file, or call setPath('/backup/files/path/') first");$filename = $path . $filename;}return $filename;}/////////////////////////////////////////////////////////////////////////////////////////////////////////////////*** Determine if exec is available for the given command** Note that WireDatabaseBackup does not currently use exec() mode so this is here for future use.** @param array $options* @return bool* @throws \Exception on unknown exec type**/protected function supportsExec(array $options = array()) {if(!$options['exec']) return false;if(empty($this->databaseConfig['dbUser'])) return false; // no db config options providedif(preg_match('{^(?:\[dbPath\])?([_a-zA-Z0-9]+)\s}', $options['execCommand'], $matches)) {$type = $matches[1];} else {throw new \Exception("Unable to determine command for exec");}if($type == 'mysqldump') {// these options are not supported by mysqldump via execif( !empty($options['excludeCreateTables']) ||!empty($options['excludeExportTables']) ||!empty($options['findReplace']) ||!empty($options['findReplaceCreateTable']) ||!empty($options['allowUpdateTables']) ||!empty($options['allowUpdate'])) {return false;}} else if($type == 'mysql') {// these options are not supported by mysql via execif( !empty($options['tables']) ||!empty($options['allowDrop']) ||!empty($options['findReplace']) ||!empty($options['findReplaceCreateTable'])) {return false;}} else {throw new \Exception("Unrecognized exec command: $type");}// first check if exec is available (http://stackoverflow.com/questions/3938120/check-if-exec-is-disabled)if(ini_get('safe_mode')) return false;$d = ini_get('disable_functions');$s = ini_get('suhosin.executor.func.blacklist');if("$d$s") {$a = preg_split('/,\s*/', "$d,$s");if(in_array('exec', $a)) return false;}// now check if mysqldump is available$o = array();$r = 0;$path = $this->databaseConfig['dbPath'];exec("{$path}$type --version", $o, $r);if(!$r && count($o) && stripos($o[0], $type) !== false && stripos($o[0], 'Ver') !== false) {// i.e. mysqldump Ver 10.13 Distrib 5.5.34, for osx10.6 (i386)return true;}return false;}}