Standardized Hooks in cPanel Using BASH – Automatic WordPress Installation

Reading Time: 13 minutes

Introduction

Who – This tutorial is targeted to cPanel users that have root accces to their cPanel server and would like to add custom functionality to cPanel and/or WHM using the Bash scripting language from the Bourne-again Shell.

What – The Standardized Hooks system in cPanel is an API that allows the systems administrator or developer to register Linux executables (often in the form of executable scripts) with cPanel so that the executable is executed at the selected hook. A hook is a specially identified event within the normal execution of cPanel or WHM. As an analogy, let’s look at what happens when you leave for work in the morning. Your normal course of events would be leaving your bedroom, on through the kitchen, out the back door, through the side gate, and finally to your car. If you were to attach a name to each of those events, you could then “register” a task that you need to remember to complete for each named event. Let’s say that you commit to remember to turn off the tv as you leave your bedroom, and lock the back door as you pass through it. If you were to say the name of each named event as you progress through them, it would trigger you to remember to execute the task that you registered in your mind with that event name.

Why – If your organization uses cPanel, and you have specialized needs that other cPanel users would not have, Standardized Hooks afford you an incredible amount of power to quickly and easily bolt on custom functionality to cPanel that works seamlessly along with the regular flow of cPanel events.

Scope – Standardized Hooks in cPanel are powerful and there is a lot of information that we could cover. The cPanel Documentation is a great resource, and provides a more thorough explantion of the feature than what you’ll get out of this tutorial. This tutorial is going to pare things down to specifically creating a basic script that installs WordPress in the public_html directory of a newly created account using a bash script. This will cover the basic concepts that you’ll need, but you should review the full documentation to see all of the other things you can do with this feature.

My personal preference is to write my code directly on the server using Vim, so uploading your script to the server is out of the scope of this tutorial. I however recommend researching SFTP if you do not want to write your code directly on the server.

NOTE: There is also an official tutorial provided by cPanel that you can reference if you would like to use Perl instead of bash: Tutorial – Creating a Standardized Hook in Perl

META

The assumption is that you already know what custom functionality you want to add to cPanel. You’ll need to think about what event in cPanel is going to be the best event to attach to in order to execute your code or application.

For ideas about what kinds of events in cPanel that you can attach to, review the documentation here: cPanel Hookable Events . Also, if you have a custom module that you’ll need to hook into, you can add additional hookable events in custom moduals: Hookable Events in Custom Modules

For the purpose of this tutorial, let’s imagine that we are a WordPress only webhosting company. We want to ensure that after account creation, the current version of WordPress is automatically installed to the new user’s public_html folder. This means that we’ll be looking to use the post hook on the Accounts::Create event from the Whostmgr events category as documented here: Whostmgr – Accounts – Create

You will also want to think about what scripting language or executable is going to best serve your needs. Some popular scripting languages that you could use are Perl, PHP, Bash, and Python. cPanel’s documenation tends to cater to Perl and PHP scripts, so those are great languages to start with.

Keep in mind that most of the executables on your Linux server will be compatible with the Standardized Hooks system. Keep reading to find out how to reboot your server everytime you attempt to create an account.

Standardized Hooks Are Dead Simple – Registering A System Binary

The process of registering an executable with a cPanel event is extremely quick and easy. All complexity is introduced when writing a custom script. To get an idea for the proccess of regsitering a prebuilt executable, let’s check out an example of registering the reboot binary with the account creation event.

WARNING: Do not do register the reboot binary unless you know the consequences and understand how to revert the change on your own. I assume no responsibility for what you do on your server, and I will not help you fix it. This is only a demonstration of the capbility of registering a system binary to be executed with cPanel events.

[root@srv00001 ~]# /usr/local/cpanel/bin/manage_hooks add script /usr/sbin/reboot --manual --category Whostmgr --event Accounts::Create --stage=pre                                     
Added hook for Whostmgr::Accounts::Create to hooks registry

[root@srv00001 ~]# ./createacct.sh #See the paragraph tilted "Testing your hook action script" for this script
username:
cpanelusernamehere
Connection to serverhostname.com closed by remote host.
Connection to serverhostname.com closed.

Initializing Your Hook Action Code Script File

Hook action code is the script that is executed at the event that you register it to.

Your hook action code must be placed at /usr/local/cpanel/3rdparty/bin, and it must have root:root ownership and 0755 permissions. To start let’s create an empty file where our code will eventually go and set the permissions:

[root@srv00001 ~]# touch /usr/local/cpanel/3rdparty/bin/wordPressAutoInstallUponAccountCreation.sh
[root@srv00001 ~]# chmod 0755 /usr/local/cpanel/3rdparty/bin/wordPressAutoInstallUponAccountCreation.sh

Troubleshooting Your Code

I like to approach new things in an incremental method, and I haven’t created a hook action script before, so let’s start off by doing a Hello World of sorts. Add the following code to your file:

[root@srv00001 ~]# cat /usr/local/cpanel/3rdparty/bin/wordPressAutoInstallUponAccountCreation.sh
#!/bin/bash
echo "Hello cPanel" > /root/hellocPanelTestFile.txt

When troubelshooting your hook action code, remember that logging errors and output are essential to determining why your script isn’t doing what you thought it should have done. In bash, this very easy. The core of it is simply understanding output redirection. Once you know how to redirect your various outputs, you can log them to a file to view after or during script execution.

Register Your Hook Action Code Script

Next, we need to register this hook action code script so that cPanel knows which event the code should be executed with. Use the command as shown below to register your hook action code script. Please review the full documentation on this command if you need to customize it to your own purposes:

[root@srv00001 ~]# /usr/local/cpanel/bin/manage_hooks add script /usr/local/cpanel/3rdparty/bin/wordPressAutoInstallUponAccountCreation.sh --manual --category Whostmgr --event Accounts::Create --stage=post 
Added hook for Whostmgr::Accounts::Create to hooks registr

The above command will register the script that we created so that it is executed on the post stage of the Create Account event. Each event has a pre and post stage so that you can execute your code before or after the event.

You can use the following command to list all of the hooks and verify that yours was added:

/usr/local/cpanel/bin/manage_hooks list

Testing Your Hook Action Code Script

You have two main options for testing your registered script. You could either login to WHM and manually create an account, or you could create a script that uses the WHM API. We’re goinng to be using the WHM API within a bash script in this tutorial, becuase it makes testing go much quicker, and takes a lot of the pain out of the process.

You can see how I created my script below:

[root@srv00001 ~]# touch createacct.sh 
[root@srv00001 ~]# chmod +x createacct.sh 
[root@srv00001 ~]# vim createacct.sh 
[root@srv00001 ~]# cat createacct.sh 
#!/bin/bash
password=$(< /dev/urandom tr -dc _A-Z-a-z-0-9 | head -c${1:-15};echo;)
echo "username:"
read username
whmapi1 createacct username=$username domain=$username.tld plan=default featurelist=default quota=0 password=$password ip=n cgi=1 hasshell=1 contactemail=devnull%40advocatehost.com cpmod=paper_lantern maxftp=5 maxsql=5 maxpop=10 maxlst=5 maxsub=1 maxpark=1 maxaddon=1 bwlimit=500 language=en useregns=1 hasuseregns=1 reseller=0 forcedns=1 mailbox_format=mdbox mxcheck=local max_email_per_hour=500 max_defer_fail_percentage=80 owner=root

When you execute this script it will prompt you for a username. It will create an account with that username and then set the domain to be the username with .tld appended. We do not need a working domain for these test accounts so this works perfectly.

Go ahead and create an account and see if your Hello cPanel file was created at /root/hellocPanelTestFile.txt

If so great! If not, you’re going to have to pay attention to errors, review the instructions above, and use the google fu.

Writing Your Hook Action Code

Getting Data from STDIN

Now that we know that our script works, and that we can output information to a file for debugging purposes, we can start getting into the meat of the project. Let’s print out the data that we get from the Account::Create event to ensure that we can get information about the account that was just created. This is important because our hook action script will need to work with the username, domain, and possibly the email address for the user. We can get that from the data provided by the event. As mentioned in the cPanel documentation, the data from the event is published to STDIN in JSON format. There are probably many ways to get this information from STDIN in bash, but this is the route that I chose for testing. I found it here. Update your hook action code to be the following:

[root@srv00001 ~]# cat /usr/local/cpanel/3rdparty/bin/wordPressAutoInstallUponAccountCreation.sh
#!/bin/bash
cat "${1:-/dev/stdin}" >> /root/debug.txt

Now create a new test account. If you cat the debug.txt file, you’ll noticed that it’s JSON that is not formatted in a human friendly way. You can use the json.tool module with python to print the JSON in human readable format:

[root@srv00001 ~]# python -m json.tool /root/debug.txt                                          
{
    "context": {
        "category": "Whostmgr",
        "event": "Accounts::Create",
        "point": "main",
        "stage": "post"
    },
    "data": {
        "bwlimit": 0,
        "contactemail": "",
        "cpmod": "paper_lantern",
        "digestauth": "n",
        "dkim": "1",
        "domain": "scripthook4.tld",
        "featurelist": "default",
        "force": null,
        "forcedns": 0,
        "gid": "",
        "hascgi": "y",
        "hasshell": "y",
        "homedir": "/home/scripthook4",
        "homeroot": "/home",
        "is_restore": 0,
        "locale": "en",
        "mailbox_format": null,
        "max_defer_fail_percentage": "unlimited",
        "max_email_per_hour": "500",
        "maxaddon": 0,
        "maxftp": "n",
        "maxlst": "n",
        "maxpark": 0,
        "maxpop": "n",
        "maxsql": "n",
        "maxsub": "n",
        "mxcheck": "local",
        "no_cache_update": 0,
        "owner": "root",
        "pass": "REDACTED",
        "plan": "default",
        "quota": "unlimited",
        "skip_mysql_dbowner_check": 0,
        "spf": "1",
        "uid": "",
        "useip": "n",
        "user": "scripthook4",
        "useregns": 0
    },
    "hook": {
        "escalateprivs": 0,
        "exectype": "script",
        "hook": "/usr/local/cpanel/3rdparty/bin/wordPressAutoInstallUponAccountCreation.sh",
        "id": "JNVpUXPPLXpR6mxcXW1d46ma",
        "stage": "post",
        "weight": 200
    }
}

Parsing JSON From a File In Bash Using Python

We can now see what kind of data we have to work with after the Account::Create event runs. In order to install the WordPress site, the script is going to need to know the username of the account that was just created. So we have to find a way to extract the username from that raw JSON string using bash. The awesome thing about using bash is the MASSIVE number of tools you have at your disposal. I did a quick search and found a stack overflow answer that gave me a quick snippet for grabbing an object from a JSON string. Update your script to have the following:

[root@srv00001 ~]# cat /usr/local/cpanel/3rdparty/bin/wordPressAutoInstallUponAccountCreation.sh 
#!/bin/bash
tmpfile=/tmp/$(date +%s)-wp-autocreate.txt
cat "${1:-/dev/stdin}" > $tmpfile
username=$(python -c "import sys, json; print json.load(open('$tmpfile'))['data']['user']")
domain=$(python -c "import sys, json; print json.load(open('$tmpfile'))['data']['domain']")
rm -f $tmpfile
echo $username > /root/debug.txt
echo $domain >> /root/debug.txt

You’ll notice that we’re putting the JSON in a temporary file so that we can get multiple values from it. I found that if I grab a value directly from STDIN with python as is shown in the stack overflow answer, the rest of the JSON was lost. In order to load the tmp file I had to modify the python code to use the open() function on the filepath.

Create another test account and then check your /root/debug.txt file to see if the username and domain was put into the debug file.

Unpacking The WordPress Tar

Okay, now that we know how to retreive the data from STDIN and get specific values from it, let’s move on to putting the wordpress files into place. Update your script with the following:

[root@srv00001 ~]# cat /usr/local/cpanel/3rdparty/bin/wordPressAutoInstallUponAccountCreation.sh 
#!/bin/bash
tmpfile=/tmp/$(date +%s)-wp-autocreate.txt
cat "${1:-/dev/stdin}" > $tmpfile
username=$(python -c "import sys, json; print json.load(open('$tmpfile'))['data']['user']")
domain=$(python -c "import sys, json; print json.load(open('$tmpfile'))['data']['domain']")
rm -f $tmpfile
docroot=/home/$username/public\_html
curl https://wordpress.org/latest.tar.gz | tar -xz --strip 1 -C $docroot
chown -R $username:$username $docroot/*

We’re piping the latest version of wordpress to the tar command to unpack it. Keep in mind that this can fail if there is an interruption in the network, or can significantly increase account creation time. A more sophisiticated system may be prudent for production use. You’ll also need to understand how your server is configured to ensure that you have set the permissions and ownership of the files correctly. On my server I just need to ensure that the files are owned by the user which is pretty typical.

NOTE: You could also use the wp cli (mentioned later in this article) to download the latest version of WordPress, but this also requires downloading data from the network. I’m utilizing this method in this example so that you can modify it to load some sort of cached wordpress version from the local server if you want.

Creating The Database And Database User

WordPress needs a database and a user. Luckily, cPanel exposes the ability to create a database and user from the command line through the cPanel User API, AKA UAPI.  I’ve already done the research on this to find out how it is done, so you can just update your script to use what I have below:

[root@srv00001 ~]# cat /usr/local/cpanel/3rdparty/bin/wordPressAutoInstallUponAccountCreation.sh 
#!/bin/bash
tmpfile=/tmp/$(date +%s)-wp-autocreate.txt
cat "${1:-/dev/stdin}" > $tmpfile
username=$(python -c "import sys, json; print json.load(open('$tmpfile'))['data']['user']")
domain=$(python -c "import sys, json; print json.load(open('$tmpfile'))['data']['domain']")
rm -f $tmpfile
docroot=/home/$username/public\_html
dbuser=$username\_wpress > /root/mysqldebug.txt
dbname=$username\_wpressdb >> /root/mysqldebug.txt
dbpass=$(< /dev/urandom tr -dc _A-Z-a-z-0-9 | head -c${1:-15};echo;) >> mysqldebug.txt
curl https://wordpress.org/latest.tar.gz | tar -xz --strip 1 -C $docroot
chown -R $username:$username $docroot/*
uapi --user=$username Mysql create_user name=$dbuser password=$dbpass
uapi --user=$username Mysql create_database name=$dbname
uapi --user=$username Mysql set_privileges_on_database user=$dbuser database=$dbname privileges='ALL PRIVILEGES'

You’ll noticed that I’m using /dev/urandom to generate the password for the database. I found this method in this How To Geek article. You can find the documentation that I used for the UAPI commands in the cPanel UAPI MySQL Module section.

In order to test to ensure that things are working the way that they should, first create another account. Then you can get the mysql credentials from the /root/mysqldebug.txt file and then test the login with the following command:

mysql -u mysqluser -p -e ‘show databases’

Setup The wp-config.php File

Now we need to setup the WordPress configuration file to prepare for the final installation. Copy the following code into your hook action code script file:

[root@srv00001 ~]# cat /usr/local/cpanel/3rdparty/bin/wordPressAutoInstallUponAccountCreation.sh  
#!/bin/bash
tmpfile=/tmp/$(date +%s)-wp-autocreate.txt
tmpfile2=/tmp/$(date +%s)-wp-salts.txt
cat "${1:-/dev/stdin}" > $tmpfile
username=$(python -c "import sys, json; print json.load(open('$tmpfile'))['data']['user']")
domain=$(python -c "import sys, json; print json.load(open('$tmpfile'))['data']['domain']")
rm -f $tmpfile
docroot=/home/$username/public\_html
dbuser=$username\_wpress
dbname=$username\_wpressdb
dbpass=$(< /dev/urandom tr -dc _A-Z-a-z-0-9 | head -c${1:-15};echo;)
wppass=$(< /dev/urandom tr -dc _A-Z-a-z-0-9 | head -c${1:-15};echo;)
wpconf=$docroot/wp-config-sample.php
curl https://wordpress.org/latest.tar.gz | tar -xz --strip 1 -C $docroot
chown -R $username:$username $docroot/*
uapi --user=$username Mysql create_user name=$dbuser password=$dbpass
uapi --user=$username Mysql create_database name=$dbname
uapi --user=$username Mysql set_privileges_on_database user=$dbuser database=$dbname privileges='ALL PRIVILEGES'
sed -i.bak -e "s/password_here/$dbpass/" $wpconf
sed -i -e "s/database_name_here/$dbname/" $wpconf
sed -i -e "s/username_here/$dbuser/" $wpconf
# Download new salts
curl "https://api.wordpress.org/secret-key/1.1/salt/" -o $tmpfile2
# Split wp-config.php into 3 on the first and last definition statements
csplit $wpconf '/AUTH_KEY/' '/NONCE_SALT/+1'
# Recombine the first part, the new salts and the last part
cat xx00 $tmpfile2 xx02 > $docroot/wp-config.php
rm -f $tmpfile2

The easy part here is inserting the database credentials. Sed is quite adept at that kind of thing.

The more difficult part was updating all of the salts. Luckily WordPress offers an API endpoint that generates that for us. Normally I try to avoid calling resources that are not local to the server. In an improved version I would optimize this to generate the salts on the server. First we grab the salts from the WordPress API, and then we use csplit to splice it into the configuration file. I found this technique on Stackoverflow.

To make sure this is working, create an account and check the wp-config file.

Viewing the WordPress Website In Browser

If you remember, we’re using fake domains with .tld as the top level domain. Now that we have a configuration file in place, it’s time to start visiting the WordPress website in the browser to see if it is doing what we want. You can update your hosts file in order to use the fake domain in your browser. cPanel has some good documenation on how this can be done.

Making Use of the WP-CLI On cPanel

cPanel packages the Word Press Command Line Interface as an RPM that you can install with YUM.  You can install wpcli with the following command on a cPanel server:
yum install wp-cli

Once you have that package installed you’ll find the wpcli binary here:
/usr/loca/cpanel/3rdparty/bin/wp

Keep in mind that you’ll need to execute the binary as the user, and your current working directory should be the document root of the wordpress site to use it.

Finalizing the WordPress Installation

Putting all of the above information together, we finally get the following script:

[root@srv00001 ~]# cat /usr/local/cpanel/3rdparty/bin/wordPressAutoInstallUponAccountCreation.sh
#!/bin/bash
tmpfile=/tmp/$(date +%s)-wp-autocreate.txt
tmpfile2=/tmp/$(date +%s)-wp-salts.txt
cat "${1:-/dev/stdin}" > $tmpfile
username=$(python -c "import sys, json; print json.load(open('$tmpfile'))['data']['user']")
domain=$(python -c "import sys, json; print json.load(open('$tmpfile'))['data']['domain']")
rm -f $tmpfile
docroot=/home/$username/public\_html
dbuser=$username\_wpress
dbname=$username\_wpressdb
dbpass=$(< /dev/urandom tr -dc _A-Z-a-z-0-9 | head -c${1:-15};echo;)
wppass=$(< /dev/urandom tr -dc _A-Z-a-z-0-9 | head -c${1:-15};echo;)
wpconf=$docroot/wp-config-sample.php
curl https://wordpress.org/latest.tar.gz | tar -xz --strip 1 -C $docroot
chown -R $username:$username $docroot/*
uapi --user=$username Mysql create_user name=$dbuser password=$dbpass
uapi --user=$username Mysql create_database name=$dbname
uapi --user=$username Mysql set_privileges_on_database user=$dbuser database=$dbname privileges='ALL PRIVILEGES'
sed -i.bak -e "s/password_here/$dbpass/" $wpconf
sed -i -e "s/database_name_here/$dbname/" $wpconf
sed -i -e "s/username_here/$dbuser/" $wpconf
# Download new salts
curl "https://api.wordpress.org/secret-key/1.1/salt/" -o $tmpfile2
# Split wp-config.php into 3 on the first and last definition statements
csplit $wpconf '/AUTH_KEY/' '/NONCE_SALT/+1'
# Recombine the first part, the new salts and the last part
cat xx00 $tmpfile2 xx02 > $docroot/wp-config.php
rm -f $tmpfile2
su -l -s /bin/bash $username -c "cd $docroot;/usr/local/cpanel/3rdparty/bin/wp core install --url=$domain --title='Interesting title' --admin_user=$username-wp --admin_password=$wppass --admin_email=replaceme"

Be sure to update the last line of this script with the appropriate email address. I won’t go into how you should populate this because that depends on your platform, and how you are going to enter your customer’s email address there. One option you have is to harvest the email address from the cPanel Account Creation JSON like we did with the username, but keep in mind that if an email address isn’t entered at the time of creation, the script will fail. You’ll need to put in some sort of validation or logic to handle this situation. The other thing that you’ll need to consider is how you are going to deliver the WordPress admin password to the user. The email that the wordpress site sends out provides the username but not the password. I would recommend using the wpcli option to disable the email, and create your own delivery method for the WordPress site credentials that is more secure than email.

Thanks

If you found this tutorial helpful, I would appreciate a shout out in the comments below. Also, please do let me know if there are things that could be improved about this tutorial in the comments below as well.

Leave a Reply

Your email address will not be published. Required fields are marked *