Created
September 15, 2015 14:15
-
-
Save tomgidden/91d4363cccb17d2ab827 to your computer and use it in GitHub Desktop.
Dependency-free PHP script to add users to Joomla via the database
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env php | |
<?php | |
// Add Super User to Joomla database. | |
// Execute './add_joomla_superuser' with no parameters for help. | |
// | |
// Thrown together by Tom Gidden <[email protected]> to scratch an itch. | |
// | |
// Yes, it could all be done via code in Joomla, but this is meant to be | |
// relatively dependency-free. We've got loads of Joomla sites using a | |
// wide range of Joomla versions, so we can't rely on the code being | |
// consistent. | |
// CLI wrapper | |
if(!defined('BATCH_ADD_JOOMLA_SUPERUSER')) { | |
$options = getopt("u:p:h:d:U:C:P:N:E:G:Xnvf1"); | |
try { | |
$ret = add_joomla_superuser($options); | |
} | |
catch (Exception $e) { | |
$help =<<<EOF | |
-u <dbuser> Database administrator user (or read from ~/.my.cnf, or ./httpdocs/configuration.php) | |
-p <dbpass> Database administrator password (or read from ~/.my.cnf, or ./httpdocs/configuration.php) | |
-h <dbhost[:port]> Database host / port (default: localhost, or from ./httpdocs/configuration.php) | |
-d <database> Joomla database name (default: read from ./httpdocs/configuration.php) | |
-U '<juser>' Joomla user name to add (or will prompt) | |
-C '<jcrypt>' Joomla pre-crypted password to add, or: | |
-P '<jpass>' Joomla password to add (or will prompt) | |
-N '<jname>' Joomla full user name (or will prompt) | |
-E '<jemail>' Joomla user email address (or will prompt) | |
-G '<jgroups>' Comma-separated list of Joomla group names (default: "Super Users") | |
-X Disable user, if exists (by Email) | |
-n Dry-run: don't actually run it; just display the SQL (implies -v) | |
-v Display the SQL executed | |
-f Force a user update if username already exists. | |
-1 Use Joomla 1.5 MD5-based password crypts | |
EOF; | |
$msg = $e->getMessage(); | |
if(!empty($msg)) | |
fwrite(STDERR, "Error: ".$msg."\n\n"); | |
fwrite(STDERR, "Syntax:\n $argv[0] <options>\n\n"); | |
fwrite(STDERR, $help); | |
exit ((int)!empty($msg)); | |
} | |
} | |
function add_joomla_superuser($options) | |
{ | |
try { | |
if(empty($options)) | |
throw new Exception(''); | |
if(isset($options['n'])) | |
$options['v'] = true; | |
// If not -n, then connect to the database | |
$dbo = false; | |
if(!isset($options['n'])) { | |
// Get MySQL user and pass | |
$db_params = get_mysql_params($options); | |
if(!isset($db_params['user'], $db_params['password'], $db_params['database'])) | |
throw new Exception("Database username/password/database not set; use -u, -p and -d"); | |
// Prepare connection string | |
$dsn = "mysql:dbname=".$db_params['database'].";host=".$db_params['host']; | |
if(isset($db_params['port'])) | |
$dsn .= ';port='.$db_params['port']; | |
// Connect to database | |
$dbo = new PDO($dsn, | |
$db_params['user'], | |
$db_params['password'], | |
array(\PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES 'UTF8'")); | |
$dbo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION); | |
// Start a transaction so our calls are all-or-nothing. | |
if( ! $dbo->beginTransaction()) | |
throw new Exception("Could not start transaction: ".json_encode($dbo->errorInfo())); | |
$quoter = function ($str) use ($dbo) { | |
return $dbo->quote($str); | |
}; | |
} | |
else { | |
// addslashes may be inadequate for database string escaping, but | |
// it's the best/easiest we've got in lieu of $dbo->quote or | |
// mysqli_real_escape_string() | |
$quoter = function ($str) { | |
return '"'.addslashes($str).'"'; | |
}; | |
} | |
if(isset($options['X'])) { | |
// For user disable, we only need the email | |
if(!isset($options['E'])) | |
$options['E'] = readline('Joomla email: '); | |
} | |
else { | |
// Get details for joomla user record | |
if(!isset($options['U'])) | |
$options['U'] = readline('Joomla username: '); | |
if(!isset($options['P']) and !isset($options['C'])) | |
$options['P'] = readline('Joomla password: '); | |
if(!isset($options['E'])) | |
$options['E'] = readline('Joomla email: '); | |
if(!isset($options['N'])) | |
$options['N'] = readline('Joomla fullname: '); | |
if(!isset($options['G'])) | |
$options['G'] = 'Super Users'; | |
} | |
// Get table prefix. Throws exception if this cannot be determined. | |
if($dbo) | |
$prefix = get_joomla_prefix($dbo); | |
else | |
$prefix = '#'; | |
// Handle user removal | |
if(isset($options['X'])) { | |
// Update user's password to '*' | |
$sql = "UPDATE ${prefix}_users SET password = '*' WHERE email = ".$quoter($options['E']); | |
if($dbo) { | |
$rows = $dbo->exec($sql); | |
if(FALSE === $rows) | |
throw new Exception("Disable failed: ".json_encode($dbo->errorInfo())); | |
else if(0 === $rows) { | |
// If this is not a batch (or batch is forced), then | |
// we need to throw an error if a deleted user is not | |
// found. | |
if(!isset($options['ignore_existing'])) { | |
throw new Exception("Disable failed; probably no user found for $options[E]."); | |
} | |
} | |
} | |
// verbose | |
if(isset($options['v'])) | |
print "$sql\n"; | |
} | |
else { | |
// Find existing user, if there is one. | |
$uid = NULL; | |
if($dbo) { | |
$users = $dbo | |
->query($sql = "SELECT id,name,email FROM ${prefix}_users WHERE username = ".$quoter($options['U']).";") | |
->fetchAll(\PDO::FETCH_ASSOC); | |
if(!isset($options['f'])) { | |
if(!isset($options['ignore_existing'])) { | |
// Not forced, so we must throw an error as the user already | |
// exists. | |
foreach ($users as $user) { | |
throw new Exception("User '$options[U]' already exists ($user[name] <$user[email]>). Use -f to override and set this user as a superuser."); | |
} | |
} | |
else { | |
if(isset($options['v'])) | |
print "$sql /* => uid already exists, but no forced update */\n"; | |
} | |
} | |
else { | |
// Forced, so get the UID to update | |
foreach ($users as $user) { | |
$uid = $user['id']; | |
break; | |
} | |
if(isset($options['v'])) | |
print "$sql /* => uid = $uid */\n"; | |
} | |
} | |
// If UID is set, then we proceed for this user. | |
if($uid) { | |
// Hash the password | |
if(isset($options['C'])) { | |
// Already crypted, so we can just use it | |
$crypt = $options['C']; | |
} | |
else if(isset($options['1'])) { | |
// Joomla 1.5 password crypting using MD5 | |
$salt = substr(base64_encode(openssl_random_pseudo_bytes(30)), 0, 32); | |
$md5 = md5($options['P'].$salt); | |
$crypt = "$md5:$salt"; | |
} | |
else { | |
// Joomla bcrypt thing, using Blowfish | |
$salt = base64_encode(openssl_random_pseudo_bytes(30)); | |
$salt = preg_replace_callback( | |
'/[^\.0-9A-Za-z\/]/', | |
function () {return sha1(openssl_random_pseudo_bytes(30))[0];}, | |
$salt); | |
$salt = '$2y$10$'.substr($salt, 0, 22); | |
$crypt = crypt($options['P'], $salt); | |
} | |
// Add user record, or update data for existing record | |
$vals = array( | |
'id' => ($uid?$uid:'NULL'), | |
'name' => $quoter($options['N']), | |
'username' => $quoter($options['U']), | |
'email' => $quoter($options['E']), | |
'password' => $quoter($crypt) | |
); | |
$sql = "INSERT INTO ${prefix}_users ". | |
"(". | |
join(',',array_keys($vals)). | |
",registerDate) ". | |
"VALUES (". | |
join(',',array_values($vals)). | |
", NOW()) ". | |
"ON DUPLICATE KEY UPDATE ". | |
join(', ',array_map(function ($k) { | |
return "$k = VALUES($k)"; | |
}, array_keys($vals))); | |
if($dbo) { | |
if(! ($q = $dbo->exec($sql)) ) { | |
$error = $dbo->errorInfo(); | |
if('00000' !== $error[0]) | |
throw new Exception("SQL $sql to add/change user failed: ".json_encode($dbo->errorInfo())); | |
} | |
} | |
// verbose | |
if(isset($options['v'])) | |
print "$sql\n"; | |
if(!empty($options['G'])) { | |
// Construct quoted, comma-separated list of group names | |
$group_list = join(',', array_map($quoter, explode(',',$options['G']))); | |
// Add group mappings | |
$sql = | |
"REPLACE INTO ${prefix}_user_usergroup_map (user_id, group_id) ". | |
"SELECT u.id, g.id ". | |
"FROM ${prefix}_usergroups g, ${prefix}_users u ". | |
"WHERE g.title IN ($group_list) ". | |
"AND u.username = ".$quoter($options['U']).";"; | |
if($dbo) { | |
$rows = $dbo->exec($sql); | |
if(FALSE === $rows) | |
throw new Exception("Group add failed: ".json_encode($dbo->errorInfo())); | |
else if(0 === $rows) | |
throw new Exception("Group add failed; probably no groups found for $group_list."); | |
} | |
// verbose | |
if(isset($options['v'])) | |
print "$sql\n"; | |
} | |
} | |
} | |
if($dbo) | |
$dbo->commit(); | |
return 0; | |
} | |
catch (Exception $e) { | |
try { | |
if(@$dbo) $dbo->rollback(); | |
} catch (PDOException $e) {} | |
throw $e; | |
} | |
} | |
function get_mysql_params($options) | |
{ | |
// Parse ./httpdocs/configuration.php, or ~/.my.cnf for login details, | |
// and override with passed options. | |
$params = array( | |
'host' => 'localhost' | |
); | |
// If run from a site's root folder (in our standard manner), load the | |
// database details from there. | |
if(is_readable('./httpdocs/configuration.php')) { | |
require_once('./httpdocs/configuration.php'); | |
$config = new JConfig(); | |
$params['host'] = $config->host; | |
$params['user'] = $config->user; | |
$params['password'] = $config->password; | |
$params['database'] = $config->db; | |
} | |
// Or from within `httpdocs` itself | |
else if(is_readable('./configuration.php')) { | |
require_once('./configuration.php'); | |
$config = new JConfig(); | |
$params['host'] = $config->host; | |
$params['user'] = $config->user; | |
$params['password'] = $config->password; | |
$params['database'] = $config->db; | |
} | |
// Or from the user's ~/.my.cnf | |
else if(is_readable($_SERVER['HOME'].'/.my.cnf')) { | |
$mycnf = parse_ini_file($_SERVER['HOME'].'/.my.cnf',true); | |
if(isset($mycnf, $mycnf['client'])) { | |
if(isset($mycnf['client']['password'])) | |
$params['password'] = $mycnf['client']['password']; | |
if(isset($mycnf['client']['user'])) | |
$params['user'] = $mycnf['client']['user']; | |
} | |
} | |
// Fill parameters, as if typed as command-line args | |
if(isset($options['u'])) | |
$params['user'] = $options['u']; | |
if(isset($options['p'])) | |
$params['password'] = $options['p']; | |
if(isset($options['h'])) | |
$params['host'] = $options['h']; | |
if(preg_match('/(.+):(\d+)$/', $params['host'], $match)) | |
list(, $params['host'], $params['port']) = $match; | |
if(isset($options['d'])) | |
$params['database'] = $options['d']; | |
return $params; | |
} | |
function get_joomla_prefix($dbo) | |
{ | |
// Scan all tables in the database. If there is one (and only one) | |
// that matches #_user_usergroup_map, then we've got '#' as our prefix | |
// (where # is 3 or more alphanumerics) | |
// | |
// If fewer or more than one are found, throw an error. Sorry. | |
$tables = $dbo->query('SHOW TABLES')->fetchAll(\PDO::FETCH_COLUMN); | |
$candidates = array(); | |
foreach ($tables as $table) { | |
if(preg_match('/(\w{3,})_user_usergroup_map/', $table, $match)) | |
$candidates[] = $match[1]; | |
} | |
switch (count($candidates)) { | |
case 0: throw new Exception("Joomla database table(s) not found"); | |
case 1: return array_shift($candidates); // okay | |
default: throw new Exception("Multiple Joomla database table sets found"); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment