#! /opt/imh-python/bin/python3
""" Helper functions for CMS manipulation """
from cpapis import cpapi2, whmapi1
LOGGER = logging.getLogger(__name__)
# Temporary functions. Should be removed when added to py rads
# from rads import common
prompt, string_filter=r'[a-zA-Z0-9._/-]+$', hint='regex', default=None
Prompt to request a string, and require it to match a regex.
If string fails to match, give a hint, which by default is just
the regex. If no matching string is obtained, return None.
If empty string is entered, return default if any exists.
Defined filters: alpha, digits, email, cpuser, database, url
if string_filter is None:
hint = 'Sorry, that should have matched.'
elif 'alpha' in string_filter:
string_filter = '[a-zA-Z0-9]+$'
hint = 'Must be only alphanumeric characters.'
elif 'digits' in string_filter:
string_filter = '[0-9.]+'
hint = 'Must be only digits.'
elif 'email' in string_filter:
r'[a-z0-9._-]+@[a-z0-9._-]+'
+ r'\.([a-z]{2,15}|xn--[a-z0-9]{2,30})$'
hint = 'Must be a valid email address.'
elif 'cpuser' in string_filter:
string_filter = '[a-z0-9]{1,14}$'
'Must be a valid cPanel user: '
+ 'letters and numbers, under 14 characters.'
elif 'database' in string_filter:
# This one is not precise, but provided for convenience.
string_filter = '[a-z0-9]{1,8}_[a-z0-9]{1,12}$'
'Must be a valid database user: '
+ 'letters and numbers, single underscore.'
elif 'url' in string_filter:
r'([a-z0-9_-]+.){1,}([a-z]{2,15}|xn--[a-z0-9]'
r'((/[a-zA-Z0-9/.%_-]*)(\?[a-zA-Z0-9/.%=;_-]+)?)?$'
hint = 'Must be a valid URL.'
except KeyboardInterrupt:
if default is not None and choice == '':
if re.match(string_filter, choice) is not None:
print('\nInvalid answer. ', end=' ')
print('\nString must match the patter: /%s/' % string_filter)
# Dictionary of dbs we have converted. This is to try to prevent
# the use of duplicate db and db user names.
'''Return home directory of cPanel user'''
if len(glob.glob("/home*/")) > 1:
result = whmapi1('accountsummary', {'user': cpuser})
if 0 == result['metadata']['result']:
"WHM API could not find home directory for %s: %s",
result['metadata']['reason'],
partition = result['data']['acct'][0]['partition']
if len(glob.glob("/var/cpanel/users/%s" % cpuser)) == 0:
"%s does not appear to be a valid cPanel user.", cpuser
docroot = f"/{partition}/{cpuser}"
def find_start_path(user_path):
Find start path from for user or path given.
LOGGER.error("No user or path specified")
requested_path = user_path
match = re.search(r"/home[^/]*/([^/]+)", user_path)
"Could not find a username in path '%s'", requested_path
username = match.group(1)
docroot = get_cp_home(username)
if re.match(docroot, requested_path) is None:
"Path given (%s) is not part of %s's document root (%s)",
if os.path.isdir(requested_path):
print("Path given does not exist: '%s'" % requested_path)
def backup_file(filename):
Find an unused filename and make a backup of a file
if not os.path.isfile(filename):
LOGGER.info("File %s does not exist to backup", filename)
date_today = datetime.datetime.utcnow().strftime("%Y-%m-%d")
new_file = f"{filename}.cms_tools.file.bak.{date_today}"
if not os.path.exists(new_file):
LOGGER.info("Copying %s -> %s", filename, new_file)
shutil.copy2(filename, new_file)
new_file = "{}.cms_tools.file.bak.{}.{}".format(
filename, date_today, num
if not os.path.exists(new_file):
LOGGER.info("Copying %s -> %s", filename, new_file)
shutil.copyfile(filename, new_file)
"File copy failed. Could not copy %s to %s: %s",
LOGGER.warning("There are already too many backup files for %s", filename)
def restore_file(source_file, destination_file):
Replace destination file with source file, removing source file
if not os.path.isfile(source_file):
LOGGER.warning("File %s does not exist. Cannot restore.", source_file)
shutil.move(source_file, destination_file)
LOGGER.info("Restored %s from %s", destination_file, source_file)
"File restore failed. Could not restore %s to %s: %s",
Read data from a file or report error and return None
LOGGER.debug("Reading from %s", filename)
if os.path.exists(filename):
with open(filename, encoding='utf-8') as file_handle:
file_data = file_handle.read()
LOGGER.error("Error reading file")
def lastmatch(regex, data):
Return the last regex match in a set of data or None
# Create the regex as a multiline
regex_object = re.compile(regex, re.M)
result = regex_object.findall(data)
def strip_php_comments(data):
Return data minus any PHP style comments
# Remove C++ style comments
data = re.sub(r"\s+//.*", "", data)
# Remove C style comments
data = re.sub(r"/\*(.*\n)*?.*?\*/", "", data)
def find_php_define(const_name, data):
Find the last instance of const_name being defined in php data
r'define\( *["\']%s["\']\s*,\s*["\']([^"\']+)["\']' % const_name, data
def find_php_var(var_name, data):
Find the last instance of var_name being assigned in php data
return lastmatch(r'\$%s\s*=\s*["\']([^"\']+)["\']' % var_name, data)
def php_re_define(const_name, value, filename):
Change all instances in filename where const_name is defined,
with open(filename, encoding='utf-8') as sources:
lines = sources.readlines()
with open(filename, "w", encoding='utf-8') as sources:
r'define\( *["\']%s["\'] *,'
r' *["\']([^"\']+)["\'] *\)' % const_name,
f"define('{const_name}','{value}')",
def php_re_assign(var_name, value, filename):
Change all instances in filename where var_name is assigned,
with open(filename, encoding='utf-8') as sources:
lines = sources.readlines()
with open(filename, "w", encoding='utf-8') as sources:
r'\$%s *= *["\']([^"\']+)["\']' % var_name,
f"${var_name} = '{value}'",
def make_valid_db_name(cpuser, prefix, current_name, name_type="database"):
Find a valid replacement database name. Typce can be database or user
LOGGER.debug("Finding new name for: %s", current_name)
# If we've already made this one, just return it
if name_type == "database":
if current_name in DB_CONVERSIONS:
return DB_CONVERSIONS[current_name]
used_names = list(DB_CONVERSIONS.values())
result = cpapi2('MysqlFE::listdbs', user=cpuser)
if 'result' in result['cpanelresult']['data']:
"cPanel API could not list databases: %s",
result['cpanelresult']['data']['reason'],
for i in result['cpanelresult']['data']:
if current_name in DB_USER_CONVERSIONS:
return DB_USER_CONVERSIONS[current_name]
used_names = list(DB_USER_CONVERSIONS.values())
result = cpapi2('MysqlFE::listdbs', user=cpuser)
if 'result' in result['cpanelresult']['data']:
"cPanel API could not list users: %s",
result['cpanelresult']['data']['reason'],
for i in result['cpanelresult']['data']:
# Add cp name only so that empty additions will fail
used_names = used_names + ["%s_" % prefix]
# For reference, this is how easily it is done with bash:
# "${cp}_$(echo "$1"|grep -Po '^([^_]*_)?\K[a-z0-9]{1,7}')"
# Sadly, Python can't handle variable-length lookbehinds
# Remove unacceptable characters
name_base = re.sub('[^a-z0-9_]', '', current_name.lower())
# Get the group after the last _
last_section = re.search('[^_]*$', name_base).group()
if len(last_section) > 4:
first_section = re.search('^[^_]*', name_base).group()
if len(first_section) > 4:
name_base = first_section
name_base = re.sub('[^a-z0-9]', '', name_base)
name_base = name_base[:8]
# Simply try the base name itself
new_name = "{}_{}".format(
re.search('^[a-z0-9]{1,%d}' % (15 - len(prefix)), name_base).group(0),
if new_name not in used_names:
if 14 > (len(prefix) + len(name_base)):
for i in range(1, (10 ** (15 - len(prefix) - len(name_base)))):
print("name base: %s" % name_base)
new_name = "{}_{}{}".format(
'^[a-z0-9]{1,%d}' % (15 - len(prefix)), name_base
if new_name not in used_names:
# If it isn't set yet, try replacing characters on the end with numbers
for i in range(len(name_base[: (15 - len(prefix) - 1)]), 1, -1):
for i in range(1, (10 ** (15 - len(prefix) - len(tmp_base)) - 1)):
new_name = "{}_{}{}".format(
'^[a-z0-9]{1,%d}' % (15 - len(prefix)), tmp_base
if new_name not in used_names: