Skip to content

Instantly share code, notes, and snippets.

@tomgidden
Created September 15, 2015 14:15
Show Gist options
  • Save tomgidden/91d4363cccb17d2ab827 to your computer and use it in GitHub Desktop.
Save tomgidden/91d4363cccb17d2ab827 to your computer and use it in GitHub Desktop.
Dependency-free PHP script to add users to Joomla via the database
#!/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