This is a very common question on the cPanel forums. Many times ‘XYZ’ is adding a particular DNS zone or creating a MySQL database. In this blog post, we’ll go through the basics of script hooks and make a post hook that utilizes the XML-API to achieve ‘XYZ.’
Script hooks are files placed into the /scripts directory and named based on the scriptโ™s functionality. Since our question revolves around account creation, the respective script is /scripts/wwwacct and our script hook file needs to be named postwwwacct. If we needed a script to execute before an account is created, we would name the script hook prewwwacct. Script hook files are handed the same parameters that are essential to the main script. The postwwwacct parameters are detailed here.
We’ll make an example post hook that will add a MySQL database, a database virtual user, and give the new database virtuser access to the new database. There are a few ways you can create these resources, however, using the XML-API is arguably the easiest solution. We’ll utilize the XML-API PHP client class to send requests to our local server. By using this client class, all the grunt work is handled for us; we just need to provide parameters to the various methods that will create our resources.
Once you’ve downloaded and unpacked the tarball, you’ll need to place xmlapi.php PHP class some place convenient. I placed in a directory in /home called cpanelscripthelpers/.
#!/usr/bin/php # Scripts hook to create database and db virtuser; pair the two <?php //set error handling so any warnings are logged ini_set('error_log','/usr/local/cpanel/logs/error_log'); ini_set('display_errors',0); //include basic xmlapi client class include('/home/cpanelscripthelpers/xmlapi.php');
We can make our script cleaner and easier to read if we extend the XML-API client class. This also makes it easier to recycle the code later if we were to decide to implement a more robust automation solution for our individual cPanel servers.
/** * extend the basic xmlapi class * add the method for getting args */ Class cpScriptsXmlApi extends XMLAPI { }
Our postwwwacct script is handed all the necessary information through the shell variable $argv. So, the first method we need to make in our extended class will parse $argv into a PHP array for easy access.
public $cliargs = array(); /** * Simple method to store args into an array * *@params array $argv shell array to be parsed *@return array */ public function argv2array ($argv) { $opts = array(); $argv0 = array_shift($argv); while(count($argv)) { $key = array_shift($argv); $value = array_shift($argv); $opts[$key] = $value; } return $opts; }
Next, since our extended class is only intended for use in script hooks, we should modify the constructor method to receive $argv and parse it.
/** * constructor * *@param array $scriptargs cli $argv that will be parsed *@param string $host *@param string $user *@param string $password *@return cpScriptsXmlApi */ public function __construct($scriptargs = array(), $host = null, $user = null, $password = null) { parent::__construct($host,$user,$password); $this->cliargs = $this->argv2array($scriptargs); return $this; }
We have three operations that need to be performed after the account is created, so lets make skeleton methods for these operations that we’ll flesh out below.
/** * Create a database */ public function createUserDb(){} /** * Create a db virtuser */ public function createDbVirtuser(){} /** * Assign db privs */ public function assignUserPrivs(){}
createUserDb() is a wrapper for the API1 (link) call adddb (link). adddb only requires the name of the new database, along with the normal parameters for any API1 call: user, module, and function.
/** * Create a database * *@param string $user cpanel user to create db as *@param string $dbname name for database */ public function createUserDb($user,$dbname){ $args = array($dbname); return $this->api1_query($user,'Mysql','adddb',$args); }
createDbVirtuser is a wrapper for the API1 call adduser (link). adduser will need the new user’s name and password. For convenience, our example is using the same password as the main account. This is ill-advised for production environments. You could easily use a pseudo-random password generator and email the new password it to the account’s address.
/** * Create a db virtuser * *@param string $user cpanel user to create virtuser as *@param string $virtusername name for db virtuser *@param string $password password for new db virtuser */ public function createDbVirtuser($user,$virtusername,$password){ $args = array($virtusername,$password); return $this->api1_query($user,'Mysql','adduser',$args); }
assignUserPrivs is a wrapper for the API1 call adddbuser (link). adddbuser needs the database, the user (in our case, the virtuser) and a comma separated list of MySQL privileges to assign. Our method actually takes an array of privileges, instead of a comma separated string, and implodes it.
/** * Assign user privs * *@param string $user cpanel user to work on behalf of *@param string $dbname name of database *@param string $virtusername receiver of privs *@param array $privs array of privileges to assign. */ public function assignUserPrivs($user,$dbname,$virtusername,$privs = array()){ $privs = (empty($privs))? array('all'): $privs; //not the best, you can change the default if you wish $priv_str = implode(',',$privs); $args = array($dbname, $virtusername, $priv_str); return $this->api1_query($user,'Mysql','adduserdb',$args); }
We now have all of the functionality necessary to perform our post account creation duties. There are just a few more things we need to setup.
XML-API instantiation:
//create our xmlapi object and set it's params $xmlapi = new cpScriptsXmlApi($argv,'127.0.0.1');
Authentication:
I like to use a root hash key. If you do not have one, use WHMโ™s Setup Remote Access Key feature (Main >> Cluster/Remote Access >> Setup Remote Access Key) to generate one. Once you have generated the key, you can find it in /root/.accesshash.
//your root auth hash $hash = file_get_contents('/root/.accesshash'); $xmlapi->set_user('root'); $xmlapi->set_hash($hash);
XML-API client setup:
I like to explicitly set my XML-API client object to use a secure port:
$xmlapi->set_port('2087'); //NOTE: you must have compiled OpenSSL or cURL w/SSL to use secure ports in this class
NOTE: All pre-made EasyApache PHP profiles do not have OpenSSL or Curl with SSL enabled by default (at least, as of Easy::Apache v3.2.0 Build 5103 ). If you want to use an SSL port (like in this example), you will need to run EasyApache with the OpenSSL or Curl with SSL option enabled, if you haven’t already done so.
Our generic database name, virtuser name, and virtuser privileges for the new database.
//generic vars for automation// $dbname = 'adb'; // "a database" $virtusername = 'ausr'; // "a user" $privs = array('all'); //you probably what to look into what you need; 'all' is very liberal
So, lets call those methods we made, providing the necessary data. First, we’ll create the database and second, we’ll create the virtuser.
$xmlapi->createUserDb($xmlapi->cliargs['user'], $dbname); $xmlapi->createDbVirtuser($xmlapi->cliargs['user'],$virtusername, $xmlapi->cliargs['pass']); //setting passwd same is not wise, but is done for example purposes ONLY
Finally, we’ll assign the virtuser to the database. But, before we do that, we need to take a little detour. cPanel 11.25.1 has a feature set called DB Mapping, which is responsible for DB Prefixing. You can find more about DB Mapping on the Integration Blog and in forthcoming cPanel documentation at documentation.cpanel.net. For now though, we’re just getting a sneak peak and laying the basis for future proof code.
In cPanel 11.25.0 and earlier, user-created databases are automatically prefixed with the user’s name, followed by an underscore and the database name. Normally, when specifying the database in the API call adduserdb, the database must be the full name (both the prefix plus its unique given name, e.g. $username_dbname). Notice in method createUserDb (API1 call adddb), we simply provided a new name. The system will add the prefix if you’re:
- Using cPanel 11.25.0 or earlier
- Using 11.25.1 or newer with Prefixing turned On (which is the default).
If you’re running 11.25.1 or greater and if Prefixing is Off, the system will try to make the literal database name we provide. This is important to know when it comes time to specify the database our virtuser can access. The virtuser name will also need this treatment
DB Prefixing is defined in /var/cpanel/cpanel.config as database_prefix.
//determine if db mapping is on $prefixing = 1; //default, may not be explicitly defined in config $config = file('/var/cpanel/cpanel.config'); foreach($config as $key=>$value){ if(stripos($value,'database_prefix=') === 0){ $prefixing = substr(trim($value),-1); // bool/int } }
Now that we know if DB Prefixing is On, we know what database and virtuser names are valid for our last method call.
$dbtoattach = ((int)$prefixing)? $xmlapi->cliargs['user'].'_'.$dbname : $dbname; $usertoattach = ((int)$prefixing)? $xmlapi->cliargs['user'].'_'.$virtusername : $virusername; $xmlapi->assignUserPrivs($xmlapi->cliargs['user'], $dbtoattach, $usertoattach, $privs); // Also, DB Mapping allows the primary db username to differ from the account name. I strongly recommend doing something like this too /* if($xmlapi->cliargs['user'] != $xmlapi->cliargs['dbuser']){ $xmlapi->assingUserPrivs($xmlapi->cliargs['user'], $dbtoattach, $xmlapi->cliargs['dbuser'], $privs); } */
For convenience, we’ve added all SQL privileges. However, you should consider limiting the privilege-set to only what is required for your particular situation.
We’re just about ready to rock-n-roll. Since DB Mapping is available in our EDGE build, and a sans-prefix cPanel configuration is quite plausible either now or in the near future, I’d re-write our examples above to be more versatile. This will include creating database resources that always add a proper prefix. This ensures that our generic names don’t fail. Remember, without prefixing, names are literal; any automation must be able to generate unique resources. At first glance, it’s a bite ugly compared to the simple implementation in the three code blocks above, but it illustrates how intricate DB Mapping can be and how best to code while keeping it in mind.
$dbname = 'adb'; // "a database" $virtusername = 'ausr'; // "a user" $privs = array('all'); //you probably what to look into what you need; 'all' is very liberal // make a prefix that allows max length of primary database username // not perfect; possible collision, but that would happen even in non-automated // db resource creation if usernames are long (cpanel softlimits to 8 char, so the point should be moot) // NOTE: db users have a MySQL hardlimit of 16 char, our generic user name are 4 char 16 - 4 - 1 for userscore = 11 usable $dbprefix = (strlen($xmlapi->cliargs['user']) > 11)? substr($xmlapi->cliargs['user'], 0,10) : $xmlapi->cliargs['user']; if( (int)$prefixing === 0 ){ // prefixing is off // double check the primary username. any prefix should be based on that name if($xmlapi->cliargs['user'] != $xmlapi->cliargs['dbuser']){ $dbprefix = (strlen($xmlapi->cliargs['dbuser']) > 11)? substr($xmlapi->cliargs['dbuser'], 0,10) : $xmlapi->cliargs['dbuser']; } //since prefixing is off, we should use literal names in both creation and assignment $xmlapi->createUserDb($xmlapi->cliargs['user'], $dbprefix.'_'.$dbname); $xmlapi->createDbVirtuser($xmlapi->cliargs['user'],$dbprefix.'_'.$virtusername, $xmlapi->cliargs['pass']); //setting passwd same is not wise, but is done per commission request $xmlapi->assignUserPrivs($xmlapi->cliargs['user'], $dbprefix.'_'.$dbname, $dbprefix.'_'.$virtusername, $privs); $xmlapi->assignUserPrivs($xmlapi->cliargs['user'], $dbprefix.'_'.$dbname, $xmlapi->cliargs['dbuser'], $privs); }else{ // prefixing is ON, default on all cpanel systems // prefix only on assignment $xmlapi->createUserDb($xmlapi->cliargs['user'], $dbname); $xmlapi->createDbVirtuser($xmlapi->cliargs['user'],$virtusername, $xmlapi->cliargs['pass']); //setting passwd same is not wise, but is done per commission request $xmlapi->assignUserPrivs($xmlapi->cliargs['user'], $dbprefix.'_'.$dbname, $dbprefix.'_'.$virtusername, $privs); }
Our script hook is now complete. If we place it in the /scripts directory and name it postwwwacct and set execution bits, it will execute each time an account is created.
You can download a copy of the complete example script at our Developer Downloads page.
All code provided on this blog is for example purposes. Please remember to properly secure your code and it’s transactions. Consider all of the functional implications it offers or exposes before placing it in a production environment.