583 lines
22 KiB
PHP
Executable File
583 lines
22 KiB
PHP
Executable File
<?php
|
|
/**
|
|
* DBSR_CLI provides a CLI interface for the DBSR class.
|
|
*/
|
|
class DBSR_CLI
|
|
{
|
|
// Static properties
|
|
/**
|
|
* Options available as parameters and their default values.
|
|
* @var array
|
|
*/
|
|
protected static $default_options = array(
|
|
'CLI' => array(
|
|
'help' => array(
|
|
'name' => array('help', 'h', '?'),
|
|
'parameter' => null,
|
|
'description' => 'display this help and exit',
|
|
'default_value' => null,
|
|
),
|
|
'version' => array(
|
|
'name' => array('version', 'v'),
|
|
'parameter' => null,
|
|
'description' => 'print version information and exit',
|
|
'default_value' => null,
|
|
),
|
|
'file' => array(
|
|
'name' => array('file', 'configfile', 'config', 'f'),
|
|
'parameter' => 'FILENAME',
|
|
'description' => 'JSON-encoded config file to load',
|
|
'default_value' => null,
|
|
),
|
|
'output' => array(
|
|
'name' => array('output', 'o'),
|
|
'parameter' => 'text|json',
|
|
'description' => 'output format',
|
|
'default_value' => 'text',
|
|
),
|
|
),
|
|
|
|
'PDO' => array(
|
|
'host' => array(
|
|
'name' => array('host', 'hostname'),
|
|
'parameter' => 'HOSTNAME',
|
|
'description' => 'hostname of the MySQL server',
|
|
'default_value' => null,
|
|
),
|
|
'port' => array(
|
|
'name' => array('port', 'portnumber'),
|
|
'parameter' => 'PORTNUMBER',
|
|
'description' => 'port number of the MySQL server',
|
|
'default_value' => null,
|
|
),
|
|
'user' => array(
|
|
'name' => array('user', 'username', 'u'),
|
|
'parameter' => 'USERNAME',
|
|
'description' => 'username used for connecting to the MySQL server',
|
|
'default_value' => null,
|
|
),
|
|
'password' => array(
|
|
'name' => array('password', 'pass', 'p'),
|
|
'parameter' => 'PASSWORD',
|
|
'description' => 'password used for connecting to the MySQL server',
|
|
'default_value' => null,
|
|
),
|
|
'database' => array(
|
|
'name' => array('database', 'db', 'd'),
|
|
'parameter' => 'DATABASE',
|
|
'description' => 'name of the database to be searched',
|
|
'default_value' => null,
|
|
),
|
|
'charset' => array(
|
|
'name' => array('charset', 'characterset', 'char'),
|
|
'parameter' => 'CHARSET',
|
|
'description' => 'character set used for connecting to the MySQL server',
|
|
'default_value' => null,
|
|
),
|
|
),
|
|
|
|
'DBSR' => array(
|
|
DBSR::OPTION_CASE_INSENSITIVE => array(
|
|
'name' => 'case-insensitive',
|
|
'parameter' => '[true|false]',
|
|
'description' => 'use case-insensitive search and replace',
|
|
'default_value' => false,
|
|
),
|
|
DBSR::OPTION_EXTENSIVE_SEARCH => array(
|
|
'name' => 'extensive-search',
|
|
'parameter' => '[true|false]',
|
|
'description' => 'process *all* database rows',
|
|
'default_value' => false,
|
|
),
|
|
DBSR::OPTION_SEARCH_PAGE_SIZE => array(
|
|
'name' => 'search-page-size',
|
|
'parameter' => 'SIZE',
|
|
'description' => 'number of rows to process simultaneously',
|
|
'default_value' => 10000,
|
|
),
|
|
DBSR::OPTION_VAR_MATCH_STRICT => array(
|
|
'name' => 'var-match-strict',
|
|
'parameter' => '[true|false]',
|
|
'description' => 'use strict matching',
|
|
'default_value' => true,
|
|
),
|
|
DBSR::OPTION_FLOATS_PRECISION => array(
|
|
'name' => 'floats-precision',
|
|
'parameter' => 'PRECISION',
|
|
'description' => 'up to how many decimals floats should be matched',
|
|
'default_value' => 5,
|
|
),
|
|
DBSR::OPTION_CONVERT_CHARSETS => array(
|
|
'name' => 'convert-charsets',
|
|
'parameter' => '[true|false]',
|
|
'description' => 'automatically convert character sets',
|
|
'default_value' => true,
|
|
),
|
|
DBSR::OPTION_VAR_CAST_REPLACE => array(
|
|
'name' => 'var-cast-replace',
|
|
'parameter' => '[true|false]',
|
|
'description' => 'cast all replace-values to the original type',
|
|
'default_value' => true,
|
|
),
|
|
DBSR::OPTION_DB_WRITE_CHANGES => array(
|
|
'name' => 'db-write-changes',
|
|
'parameter' => '[true|false]',
|
|
'description' => 'write changed values back to the database',
|
|
'default_value' => true,
|
|
),
|
|
DBSR::OPTION_HANDLE_SERIALIZE => array(
|
|
'name' => 'handle-serialize',
|
|
'parameter' => '[true|false]',
|
|
'description' => 'interpret serialized strings as their PHP types',
|
|
'default_value' => true,
|
|
),
|
|
DBSR::OPTION_LOCK_TABLES => array(
|
|
'name' => 'lock-tables',
|
|
'parameter' => '[true|false]',
|
|
'description' => 'lock tables when running',
|
|
'default_value' => true,
|
|
),
|
|
),
|
|
);
|
|
|
|
// Static methods
|
|
/**
|
|
* Prints the version string.
|
|
*/
|
|
public static function printVersion()
|
|
{
|
|
echo 'DBSR ' . DBSR::VERSION . ' CLI, running on PHP ', PHP_VERSION, ' (', \PHP_SAPI, '), ', PHP_OS, '.', "\n";
|
|
}
|
|
|
|
/**
|
|
* Prints the help text based on $default_options.
|
|
*
|
|
* @param $filename The filename to display, NULL for autodetect using $argv.
|
|
*/
|
|
public static function printHelp($filename = null)
|
|
{
|
|
$pad_left = 4;
|
|
$width_left = 40;
|
|
$width_right = 32;
|
|
if (null === $filename) {
|
|
if (isset($_SERVER['argv']) && is_array($_SERVER['argv'])) {
|
|
$filename = $_SERVER['argv'][0];
|
|
} else {
|
|
$filename = basename($_SERVER['SCRIPT_NAME']);
|
|
}
|
|
}
|
|
|
|
static::printVersion();
|
|
|
|
echo "\n",
|
|
'Usage: ', $filename, ' [options] -- SEARCH REPLACE [SEARCH REPLACE...]', "\n" .
|
|
' ', $filename, ' --file FILENAME', "\n" .
|
|
"\n";
|
|
foreach (static::$default_options as $name => $optionset) {
|
|
echo $name, ' options:', "\n";
|
|
foreach ($optionset as $option) {
|
|
// Force type to array
|
|
$option['name'] = (array) $option['name'];
|
|
|
|
// Option
|
|
$parameter = (strlen($option['name'][0]) > 1 ? '--' : '-') . $option['name'][0];
|
|
|
|
// Parameter
|
|
if (null !== $option['parameter']) {
|
|
$parameter .= ' ' . $option['parameter'];
|
|
}
|
|
|
|
// Description
|
|
$description_array = preg_split('/(.{1,' . $width_right . '}(?:\s(?!$)|(?=$)))/', $option['description'], null, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE);
|
|
$description = $description_array[0];
|
|
for ($i = 1; $i < count($description_array); $i++) {
|
|
$description .= "\n" . str_repeat(' ', $width_left + $pad_left) . $description_array[$i];
|
|
}
|
|
|
|
// Default
|
|
if (null !== $option['default_value']) {
|
|
$default = $option['default_value'];
|
|
if (is_bool($default)) {
|
|
$default = $default ? 'true' : 'false';
|
|
} else {
|
|
$default = (string) $default;
|
|
}
|
|
$default = ' (default: ' . $default . ')';
|
|
if (strlen($description_array[count($description_array) - 1]) + strlen($default) > $width_right) {
|
|
$description .= "\n" . str_repeat(' ', $width_left + $pad_left - 1);
|
|
}
|
|
$description .= $default;
|
|
}
|
|
|
|
// Echo the option
|
|
echo str_repeat(' ', $pad_left), str_pad($parameter, $width_left), $description, "\n";
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns the corresponding default option given a switch name.
|
|
*
|
|
* @param string $switch The switch to search for.
|
|
* @param boolean $check_prefix Whether to check for the correct prefix.
|
|
* @return mixed The option array, or FALSE if the switch was not found.
|
|
*/
|
|
protected static function getOption($switch, $check_prefix = true)
|
|
{
|
|
foreach (static::$default_options as $setname => $set) {
|
|
foreach ($set as $id => $option) {
|
|
foreach ((array) $option['name'] as $name) {
|
|
if ($switch == ($check_prefix ? (strlen($name) > 1 ? ('--' . $name) : ('-' . $name)) : $name)) {
|
|
$option['set'] = $setname;
|
|
$option['id'] = $id;
|
|
return $option;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// Properties
|
|
/**
|
|
* @var PDO
|
|
*/
|
|
protected $pdo;
|
|
|
|
/**
|
|
* @var DBSR
|
|
*/
|
|
protected $dbsr;
|
|
|
|
/**
|
|
* Options currently set.
|
|
* @var array
|
|
*/
|
|
protected $options = array();
|
|
|
|
/**
|
|
* The search-values.
|
|
* @var array
|
|
*/
|
|
protected $search = array();
|
|
|
|
/**
|
|
* The replace-values.
|
|
* @var array
|
|
*/
|
|
protected $replace = array();
|
|
|
|
/**
|
|
* List of configfiles which have been processed.
|
|
* Used to reprevent recursive inclusion.
|
|
*
|
|
* @var array
|
|
*/
|
|
private $configfiles = array();
|
|
|
|
// Methods
|
|
/**
|
|
* Constructor: builds a new DBSR_CLI object and initalizes all options to their defaults
|
|
*/
|
|
public function __construct()
|
|
{
|
|
foreach (static::$default_options as $setname => $set) {
|
|
foreach ($set as $id => $option) {
|
|
if (null !== $option['default_value']) {
|
|
$this->options[$setname][$id] = $option['default_value'];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Executes DBSR with the currently set options. Does not return but dies with the result.
|
|
*/
|
|
public function exec()
|
|
{
|
|
// Prepare the DSN and PDO options array
|
|
$pdo_options = array(
|
|
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
|
);
|
|
|
|
$dsn = 'mysql:';
|
|
if (isset($this->options['PDO']['host'])) {
|
|
$dsn .= 'host=' . $this->options['PDO']['host'];
|
|
if (isset($this->options['PDO']['port'])) {
|
|
$dsn .= ':' . $this->options['PDO']['port'];
|
|
}
|
|
$dsn .= ';';
|
|
}
|
|
if (isset($this->options['PDO']['database'])) {
|
|
$dsn .= 'dbname=' . $this->options['PDO']['database'] . ';';
|
|
}
|
|
if (isset($this->options['PDO']['charset'])) {
|
|
$pdo_options[PDO::MYSQL_ATTR_INIT_COMMAND] = 'SET NAMES ' . $this->options['PDO']['charset'];
|
|
$dsn .= 'charset=' . $this->options['PDO']['charset'] . ';';
|
|
}
|
|
|
|
try {
|
|
// Try building a PDO, DBSR and running the search- and replace-action
|
|
$this->pdo = new PDO($dsn, @$this->options['PDO']['user'], @$this->options['PDO']['password'], $pdo_options);
|
|
$this->dbsr = new DBSR($this->pdo);
|
|
|
|
// Set the DBSR options
|
|
foreach ($this->options['DBSR'] as $option => $value) {
|
|
$this->dbsr->setOption($option, $value);
|
|
}
|
|
|
|
// Set the search- and replace-values
|
|
$this->dbsr->setValues($this->search, $this->replace);
|
|
|
|
// Execute DBSR
|
|
$result = $this->dbsr->exec();
|
|
} catch (Exception $e) {
|
|
// Check the output type for the exception
|
|
switch ($this->options['CLI']['output']) {
|
|
case 'json':
|
|
exit(json_encode(array('error' => $e->getMessage())));
|
|
|
|
case 'text':
|
|
default:
|
|
exit($e->getMessage());
|
|
}
|
|
}
|
|
|
|
// Output the result
|
|
switch ($this->options['CLI']['output']) {
|
|
case 'text':
|
|
exit('Result: ' . $result . ' rows were ' . ($this->options['DBSR'][DBSR::OPTION_DB_WRITE_CHANGES] ? 'changed' : 'matched (no changes were written to the databasse)') . '!');
|
|
|
|
case 'json':
|
|
exit(json_encode(array('result' => $result)));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Parses command line arguments. Directly outputs and dies in case of errors.
|
|
*
|
|
* @param array $arguments The array of arguments, the first element being the script's filename.
|
|
*/
|
|
public function parseArguments(array $arguments)
|
|
{
|
|
if (empty($arguments)) {
|
|
if (isset($_SERVER['argv']) && is_array($_SERVER['argv'])) {
|
|
$arguments = $_SERVER['argv'];
|
|
} else {
|
|
$arguments = array(basename($_SERVER['SCRIPT_NAME']));
|
|
}
|
|
}
|
|
|
|
// Check if there are no arguments
|
|
if (count($arguments) <= 1) {
|
|
echo 'Usage: ', $arguments[0], ' [options] -- SEARCH REPLACE [SEARCH REPLACE...]', "\n" .
|
|
' ', $arguments[0], ' --file FILENAME', "\n" .
|
|
'Try `', $arguments[0], ' --help` for more information.', "\n";
|
|
exit;
|
|
}
|
|
|
|
// Loop over all arguments
|
|
for ($i = 1; $i < count($arguments); $i++) {
|
|
switch ($arguments[$i]) {
|
|
case '--':
|
|
// Check the number of search- and replace-values
|
|
if (count($arguments) - 1 - $i == 0) {
|
|
exit('Missing search- and replace-values!');
|
|
}
|
|
if ((count($arguments) - 1 - $i) % 2 != 0) {
|
|
exit('Missing replace-value for seach-value: ' . (string) $arguments[count($arguments) - 1]);
|
|
}
|
|
|
|
// Save all search- and replace-values
|
|
for (++$i; $i < count($arguments); $i++) {
|
|
$this->search[] = $arguments[$i];
|
|
$this->replace[] = $arguments[++$i];
|
|
}
|
|
break;
|
|
|
|
default:
|
|
// Get the option
|
|
$option = static::getOption($arguments[$i]);
|
|
if (!$option) {
|
|
exit('Unknown argument: ' . (string) $arguments[$i]);
|
|
}
|
|
|
|
// Check for any arguments
|
|
if (null !== $option['parameter']) {
|
|
$arg = @$arguments[$i + 1];
|
|
|
|
// Boolean value without argument?
|
|
if (is_bool($option['default_value']) && (null === $arg || preg_match('/^\-/', $arg))) {
|
|
$this->options[$option['set']][$option['id']] = !$option['default_value'];
|
|
break;
|
|
}
|
|
|
|
// Missing argument?
|
|
if (null === $arg || preg_match('/^\-/', $arg)) {
|
|
exit('Missing option for ' . (string) $arguments[$i]);
|
|
}
|
|
|
|
// Special cases
|
|
switch ($option['set'] . '/' . $option['id']) {
|
|
case 'CLI/output':
|
|
if ($arg == 'json' && !extension_loaded('json')) {
|
|
exit('Error: The PHP JSON extension is not available!');
|
|
}
|
|
break;
|
|
case 'CLI/file':
|
|
if (!extension_loaded('json')) {
|
|
exit('Error: The PHP JSON extension is not available!');
|
|
}
|
|
if (!$this->parseConfig($arg)) {
|
|
exit('Failed to parse config file: ' . (string) $arg);
|
|
}
|
|
$i++;
|
|
break 2;
|
|
}
|
|
|
|
// Parse the argument
|
|
if (null !== $option['default_value']) {
|
|
// Special cases and specific error messages
|
|
if (is_bool($option['default_value'])) {
|
|
if (strtolower($arg) == 'true') {
|
|
$arg = true;
|
|
} elseif (strtolower($arg) == 'false') {
|
|
$arg = false;
|
|
} elseif (is_numeric($arg)) {
|
|
$arg = (bool) (int) $arg;
|
|
} else {
|
|
exit('Invalid argument, expected boolean for ' . (string) $arguments[$i]);
|
|
}
|
|
} elseif (is_int()) {
|
|
if (is_numeric($arg)) {
|
|
$arg = (int) $arg;
|
|
} else {
|
|
exit('Invalid argument, expected integer for ' . (string) $arguments[$i]);
|
|
}
|
|
} elseif (is_float()) {
|
|
if (is_numeric($arg)) {
|
|
$arg = (float) $arg;
|
|
} else {
|
|
exit('Invalid argument, expected float for ' . (string) $arguments[$i]);
|
|
}
|
|
}
|
|
|
|
// Typeset
|
|
settype($arg, gettype($option['default_value']));
|
|
}
|
|
|
|
// Save the argument
|
|
$this->options[$option['set']][$option['id']] = $arg;
|
|
$i++;
|
|
} else {
|
|
// Special cases
|
|
switch ($option['set'] . '/' . $option['id']) {
|
|
case 'CLI/help':
|
|
exit(static::printHelp($arguments[0]));
|
|
|
|
case 'CLI/version':
|
|
exit(static::printVersion());
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Parses a config file
|
|
* @param string $file Path to the config file.
|
|
*/
|
|
public function parseConfig($file)
|
|
{
|
|
// Check if the file exists
|
|
if (!file_exists($file) || !realpath($file)) {
|
|
return false;
|
|
}
|
|
|
|
// Check if we've read the file before
|
|
if (in_array(realpath($file), $this->configfiles)) {
|
|
return false;
|
|
}
|
|
$this->configfiles[] = realpath($file);
|
|
|
|
// Read file contents
|
|
$file_contents = @file_get_contents($file);
|
|
if (!$file_contents) {
|
|
return false;
|
|
}
|
|
|
|
// Decode content
|
|
$file_array = json_decode($file_contents, true);
|
|
if (!is_array($file_array)) {
|
|
return false;
|
|
}
|
|
|
|
// Load search- and replace-values (if existing)
|
|
if (isset($file_array['search']) && is_array($file_array['search'])) {
|
|
$this->search += $file_array['search'];
|
|
}
|
|
if (isset($file_array['replace']) && is_array($file_array['replace'])) {
|
|
$this->replace += $file_array['replace'];
|
|
}
|
|
|
|
// Check for options
|
|
if (isset($file_array['options']) && is_array($file_array['options'])) {
|
|
// Return success
|
|
return $this->_parseConfigArray($file_array['options']);
|
|
} else {
|
|
// No options, no problems
|
|
return true;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* parseConfig: runs through an option array and parses every option.
|
|
*
|
|
* @param array $array The array of options
|
|
* $return boolean TRUE if the array was parsed succesfully, FALSE otherwise.
|
|
*/
|
|
private function _parseConfigArray(array $array)
|
|
{
|
|
foreach ($array as $key => $element) {
|
|
if (is_array($element)) {
|
|
if (!$this->_parseConfigArray($element)) {
|
|
return false;
|
|
}
|
|
} else {
|
|
// Check the option
|
|
$option = static::getOption($key, false);
|
|
if (!$option) {
|
|
return false;
|
|
}
|
|
|
|
// Special cases without paramaters
|
|
switch ($option['set'] . '/' . $option['id']) {
|
|
case 'CLI/help':
|
|
exit(static::printHelp());
|
|
|
|
case 'CLI/version':
|
|
exit(static::printVersion());
|
|
}
|
|
|
|
// No parameter? No go!
|
|
if (null === $option['parameter']) {
|
|
return false;
|
|
}
|
|
|
|
// Special cases with paramaters
|
|
switch ($option['set'] . '/' . $option['id']) {
|
|
case 'CLI/file':
|
|
if (!$this->parseConfig($element)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Save the value
|
|
$this->options[$option['set']][$option['id']] = $element;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
}
|