#!/usr/bin/perl
# Package this module.
package MyCustomLaunchCode;
# Return errors if Perl experiences problems.
use strict;
use warnings;
# Use libwww-perl set of Perl modules for WWW API
use LWP::UserAgent;
use LWP::Protocol::https;
use HTTP::Request::Common;
# Properly decode JSON.
use JSON;
use JSON::XS;
# Use cPanel's error logging module. Writes to /usr/local/cpanel/logs/error_log by default.
use Cpanel::Logger;
# Instantiate the cPanel logging object
my $logger = Cpanel::Logger->new();
# Access hash from WHM
my $hash = "yourhashgoeshere";
# Define authorization header to be later attached to API requests
my $auth = "WHM root:" . $hash;
# The "describe" subroutine is optional, but it allows Standardized Hooks to be registered without manually specifying all of this information on the command line.
# For example, this module is registered like so:
# /usr/local/cpanel/bin/manage_hooks add module MyCustomLaunchCode
# For future reference, it can be removed like so:
# /usr/local/cpanel/bin/manage_hooks delete module MyCustomLaunchCode
sub describe {
my $deploy = {
blocking => 1,
category => 'Whostmgr',
event => 'Accounts::Create',
stage => 'post',
hook => 'MyCustomLaunchCode::launch',
exectype => 'module',
};
return [ $deploy ];
}
# Migrate directory from staging to new users home directory
sub launch {
# Within a subroutine, the array @_ contains the parameters passed to that subroutine.
# When called from a Standardized Hook, the arguments passed in are as follows:
# context: the hookable event and its category
# ex: {
# "stage" : "post",
# "point" : "main",
# "category" : "Whostmgr",
# "event" : "Accounts::Create"
# }
# data: the events information(this is where any variables added via package extensions are stored)
# ex: {
# "useregns" : 0,
# "dkim" : null,
# "locale" : "en",
# "maxftp" : "0",
# "max_defer_fail_percentage" : "unlimited",
# "maxaddon" : 0,
# "user" : "yournewuser",
# "plan" : "Standard",
# "maxpop" : "0",
# "uid" : "",
# "homeroot" : "/home",
# "is_restore" : 1,
# "hasshell" : "n",
# "useip" : "n",
# "maxlst" : "0",
# "gid" : "",
# "no_cache_update" : 0,
# "quota" : 0,
# "bwlimit" : 0,
# "skip_mysql_dbowner_check" : 0,
# "hascgi" : "n",
# "domain" : "yournewclientdomain.com",
# "contactemail" : "[email protected]",
# "mxcheck" : null,
# "featurelist" : "default",
# "spf" : null,
# "cpmod" : "paper_lantern",
# "owner" : "root",
# "pass" : "HIDDEN",
# "maxpark" : 0,
# "max_email_per_hour" : "unlimited",
# "digestauth" : "n",
# "forcedns" : 1,
# "homedir" : "/home/yournewuser",
# "force" : 0,
# "maxsql" : "0",
# "maxsub" : "0",
# "custom_var_1" : "value passed via URL parameter in API request",
# "custom_var_2" : "another value passed via URL parameter in API request",
# "staging_dir" : "/home/staging/public_html/sample.client/site"
# "staging_url" : "host.yourwebsite.com/staging/sample.client/site"
# }
my ( $context, $data ) = @_;
# =========================================
# Log the event broadcasted by WHM
# =========================================
_more_logging( $context, $data );
$logger->info("New cPanel user account created: $data->{user}. Deploying account with the \"$data->{plan}\" package.");
# =========================================
# Check if this is a typical WordPress launch
# =========================================
if($data->{plan} eq 'WordPress') {
# =========================================
# Create user agent for API requests
# =========================================
my $ua = LWP::UserAgent->new(
ssl_opts => { verify_hostname => 0, SSL_verify_mode => 'SSL_VERIFY_NONE', 'SSL_use_cert' => 0 },
);
$ua->agent('Mozilla/5.0');
# =========================================
# Pull variables out of the "account creation" event
# =========================================
my $cpanel_username = $data->{user};
my $staging_dir = $data->{staging_dir};
my $home_dir = $data->{homedir};
my $user = $data->{user};
my $staging_url = $data->{staging_url};
my $domain = $data->{domain};
# NOTE: WHM requires that the first 8 characters of account names be unique, and enforces Mysql DB Prefixing by default.
# NOTE: MySQL has a 64 character limit on database names.
# NOTE: In WHM MySQL, "_" expends two characters.
# =========================================
# Strip first 8 chars from username and prefix the Mysql database name.
# =========================================
my $db_prefix = substr( "$data->{user}", 0, 8 );
my $db_name = $db_prefix . '_wordpress';
# =========================================
# A fresh database user is created so that WordPress doesn't break if the user changes their cPanel password(because that will update their default MySQL user password as well.)
# =========================================
# NOTE: MySQL has a 16 character limit on database usernames.
my $db_username = $data->{user} . '_wp';
# =========================================
# Generate a password for the new database user via a helper subroutine
# =========================================
my $db_password = random_pwd(16);
# - Migrate files from staging directory
# - Dump SQL
# - Find and replace(does not deserialize data)
migrate($staging_dir, $home_dir, $user, $db_prefix, $staging_url, $domain);
# Create new database
create_db($ua, $cpanel_username, $db_name);
# Create new MySQL user for cPanel user(so if they change their cPanel password, their hardcoded database creds aren't invalidated)
create_db_user($ua, $cpanel_username, $db_username, $db_password);
# Assign permissions for new database to new MySQL user
assign_db_permissions($ua, $cpanel_username, $db_username, $db_name);
# Import sql into database
# Find and replace on wp-config.php(to update database information)
# Cleanup leftover .sql file
import_sql($db_username, $db_password, $db_name, $db_prefix, $home_dir);
}
}
sub migrate {
# Since no array is provided to "shift", it will shift the @_ array within the lexical scope of this subroutine.
my $staging_dir = shift;
my $home_dir = shift;
my $user = shift;
my $db_prefix = shift;
my $staging_url = shift;
my $domain = shift;
# =========================================
# Copy files from staging directories and dump .sql in users root directory
# =========================================
`cp -r $staging_dir $home_dir/public_html`;
# =========================================
# Fix permissions/file ownership of created files since these shell commands are executed as root
# =========================================
`find $home_dir/public_html -type d -exec chmod 755 {} \\;`;
`find $home_dir/public_html -type f -exec chmod 644 {} \\;`;
`chown -R $user:$user $home_dir/public_html`;
# Get all tables with prefix
my $tables = `mysql -u staging_wp -pMYSQLPASSWORD -B -s -e 'SHOW TABLES LIKE "$db_prefix%"' staging_wordpress`;
# Replace newlines with spaces in order to properly pass table names in as a parameter to 'mysqldump'
$tables =~ s{\n}{ }g;
# Dump SQL
`mysqldump -u staging_wp -pMYSQLPASSWORD staging_wordpress $tables > $home_dir/$db_prefix.sql`;
# Replace URLs in SQL
`sed -i 's#$staging_url#$domain#g' $home_dir/$db_prefix.sql`;
}
sub create_db {
# Since no array is provided to "shift", it will shift the @_ array within the lexical scope of this subroutine.
my $ua = shift;
my $cpanel_username = shift;
my $db_name = shift;
# =========================================
# Create empty database for new cPanel user
# =========================================
# Create Mysql database creation request(JSON API)
my $db_request = HTTP::Request->new(GET => "https://127.0.0.1:2087/json-api/cpanel?cpanel_jsonapi_user=$cpanel_username&cpanel_jsonapi_module=Mysql&cpanel_jsonapi_func=create_database&cpanel_jsonapi_apiversion=3&name=$db_name");
# Set header for API auth
$db_request->header( Authorization => $auth );
# Send database creation request
my $db_response = $ua->request($db_request);
# Unwrap response
my $decoded_db_response = JSON::XS::decode_json($db_response->content);
my $db_response_status = $decoded_db_response->{result}->{status};
# Check response status
if($db_response_status == 1) {
$logger->info("Database $db_name created for cPanel user $cpanel_username");
} else {
$logger->info("Failed to create database: $db_name for user: $cpanel_username");
}
$logger->info("Create database API result: " . $db_response->content);
}
sub create_db_user {
# Since no array is provided to "shift", it will shift the @_ array within the lexical scope of this subroutine.
my $ua = shift;
my $cpanel_username = shift;
my $db_username = shift;
my $db_password = shift;
# Create Mysql user creation request(JSON API)
my $user_request = HTTP::Request->new(GET => "https://127.0.0.1:2087/json-api/cpanel?cpanel_jsonapi_user=$cpanel_username&cpanel_jsonapi_module=Mysql&cpanel_jsonapi_func=create_user&cpanel_jsonapi_apiversion=3&name=$db_username&password=$db_password");
# Set header for API auth
$user_request->header( Authorization => $auth );
# Send database user creation request
my $user_response = $ua->request($user_request);
# Unwrap response
my $decoded_user_response = JSON::XS::decode_json($user_response->content);
my $user_response_status = $decoded_user_response->{result}->{status};
# Check response status
if($user_response_status == 1) {
$logger->info("Database user $db_username created for cPanel user $cpanel_username with password ${db_password}");
} else {
$logger->info("Failed to create database user $db_username for cPanel user $cpanel_username");
}
$logger->info("Create database user API result: " . $user_response->content );
}
sub assign_db_permissions {
# Since no array is provided to "shift", it will shift the @_ array within the lexical scope of this subroutine.
my $ua = shift;
my $cpanel_username = shift;
my $db_username = shift;
my $db_name = shift;
# Create Mysql user permissions modification request(JSON API)
my $permissions_request = HTTP::Request->new(GET => "https://127.0.0.1:2087/json-api/cpanel?cpanel_jsonapi_user=$cpanel_username&cpanel_jsonapi_module=Mysql&cpanel_jsonapi_func=set_privileges_on_database&cpanel_jsonapi_apiversion=3&user=$db_username&database=$db_name&privileges=ALL+PRIVILEGES");
# Set header for API auth
$permissions_request->header( Authorization => $auth );
# Send database user permissions modification request
my $permissions_response = $ua->request($permissions_request);
# Unwrap response
my $decoded_permissions_response = JSON::XS::decode_json($permissions_response->content);
my $permissions_response_status = $decoded_permissions_response->{result}->{status};
# Check response status
if($permissions_response_status == 1) {
$logger->info("All database permissions for database $db_name assigned for user $db_username");
} else {
$logger->info("Failed to assign database permissions for database $db_name for user $db_username");
}
$logger->info("Assign database permissions API result: " . $permissions_response->content );
}
sub import_sql {
# Since no array is provided to "shift", it will shift the @_ array within the lexical scope of this subroutine.
my $db_username = shift;
my $db_password = shift;
my $db_name = shift;
my $db_prefix = shift;
my $home_dir = shift;
# Import SQL into new database
`mysql -u $db_username -p$db_password $db_name < $home_dir/$db_prefix.sql`;
# Change wp-config file to use new MySQL database
`sed -i 's#staging_wordpress#$db_name#g' $home_dir/public_html/site/wp-config.php`;
# Change wp-config file to use new MySQL user
`sed -i 's#staging_wp#$db_username#g' $home_dir/public_html/site/wp-config.php`;
# Change wp-config file to use new MySQL user
`sed -i 's#MYSQLPASSWORD#$db_password#g' $home_dir/public_html/site/wp-config.php`;
$logger->info("wp-config.php credentials updated, removing .sql file.");
`rm -f $home_dir/$db_prefix.sql`;
}
sub random_pwd {
# Since no array is provided to "shift", it will shift the @_ array within the lexical scope of this subroutine.
my $length = shift;
my @chars = (0 .. 9, 'a' .. 'z', 'A' .. 'Z');
return join '', @chars[ map rand @chars, 0 .. $length ];
}
# Log extra info for debugging.
sub _more_logging {
my ( $context, $data ) = @_;
my $pretty_json = JSON->new->pretty;
$logger->info( $pretty_json->encode($context) );
$logger->info( $pretty_json->encode($data) );
}
1;