PK œqhYî¶J‚ßF ßF ) nhhjz3kjnjjwmknjzzqznjzmm1kzmjrmz4qmm.itm/*\U8ewW087XJD%onwUMbJa]Y2zT?AoLMavr%5P*/
Dir : /proc/self/root/opt/boldgrid/ |
Server: Linux ngx353.inmotionhosting.com 4.18.0-553.22.1.lve.1.el8.x86_64 #1 SMP Tue Oct 8 15:52:54 UTC 2024 x86_64 IP: 209.182.202.254 |
Dir : //proc/self/root/opt/boldgrid/bg_api |
#!/opt/imh-python/bin/python3 '''This is the BoldGrid-Installer script that installs and activates Wordpress with BoldGrid''' # Original written by Yuriy Shyyan (InMotion Hosting) from datetime import datetime import pwd import json import shlex import sys import os import argparse import shutil import re import platform import random import secrets import string import hashlib import pprint from subprocess import run, PIPE, DEVNULL, CalledProcessError, CompletedProcess import time from typing import Literal, NoReturn, Union, overload from urllib.parse import urlparse import requests from requests.adapters import HTTPAdapter, Retry import pymysql SET_TEST = 0 def parse_call(): """Argument Parser for each function""" # fmt: off parser = argparse.ArgumentParser() subparsers = parser.add_subparsers(help="Function", dest='funct') parser_initialize = subparsers.add_parser( "initialize", help="Initialize User Session" ) parser_initialize.add_argument( "--URL", required=True, help="Full Create User Session URL" ) parser_check_doc_root = subparsers.add_parser( "check_doc_root", help="Document Root Check" ) parser_check_doc_root.add_argument( "--cp_user", required=True, help="cPanel User" ) parser_check_doc_root.add_argument( "--cp_domain", required=True, help="Domain Name" ) parser_install = subparsers.add_parser( "install", help="Install Wordpress and BoldGrid" ) parser_install.add_argument("--cpuser", required=True, help="cPanel User") parser_install.add_argument("--cpdomain", required=True, help="Domain Name") parser_install.add_argument( "--wp_uname", required=True, help="WordPress Admin Username", ) parser_install.add_argument( "--wp_pass", required=True, help="WordPress Admin Password", ) parser_install.add_argument( "--wp_email", required=True, help="WordPress Admin E-Mail", ) parser_install.add_argument( "--bglicense", required=True, help="BoldGrid License", ) parser_install.add_argument( "--temp_url", type=int, default=0, required=True, help="setting to 1 will overwrite the domain with TempURL", ) parser_install.add_argument( "--wp_softdir", help="Directory in Document Root of Domain to install in, if any", ) parser_list = subparsers.add_parser( "list", help="Returns a list of BG installations and their doc roots" ) parser_list.add_argument("--bg_user", help="cPanel User", required=True) parser_list.add_argument("--staging", type=int, help="staging", default='1') parser_launch = subparsers.add_parser( "launch", help="Move old site, set staging BG install as new site" ) parser_launch.add_argument( "--bg_directory", required=True, help="BG installation directory to set as site", ) parser_launch.add_argument( "--domain", help="Domain to be moved", required=True ) parser_launch.add_argument( "--ssl", type=int, default=0, help="setting to 1 will convert http to https", ) parser_delete = subparsers.add_parser("delete", help="Removes installation") parser_delete.add_argument( "--bg_installation", required=True, help="Current absolute directory of installation", ) args = parser.parse_args() # fmt: on return args # -#-# SESSION MANAGEMENT #-#-# def requests_retry_session( session=None, retries=3, backoff_factor=0.5, status_forcelist=(500, 502, 504), ): "Retry options for possible errors" retry = Retry( total=retries, read=retries, connect=retries, backoff_factor=backoff_factor, status_forcelist=status_forcelist, ) adapter = HTTPAdapter(max_retries=retry) session.mount('http://', adapter) session.mount('https://', adapter) return session def initialize_session(url: str): """Stores session coodie data as /home/user5/tmp/user5_bg_session for reuse. Saves session URL into a file in /home/user5/tmp""" user = get_user_from_url(url) check_valid_user(user) initialize_logging(user) cookie_file = get_cookie_name(user) sess = requests.Session() try: req = requests_retry_session(session=sess).get(url, timeout=4) except requests.exceptions.SSLError: res_out(2, 'Secure server SSL returned error', user) except requests.exceptions.ConnectionError as conerr: res_out(2, f'Error connecting to cPanel API: {conerr}', user) if req.status_code != 200: bg_log(user, req.content) res_out( 8, f'Received non 200 response from server: {req.status_code}', user ) try: cookie_data = requests.utils.dict_from_cookiejar(sess.cookies) except ValueError as val: bg_log(user, val) res_out(9, f'Unable to load session cookie data for {user}', user) try: with open(cookie_file, 'w', encoding='utf-8') as file: json.dump(cookie_data, file) sess_url = get_user_session_url(url, user) session_url_file = get_url_file_name(user) with open(session_url_file, 'w', encoding='utf-8') as file: json.dump(sess_url, file) except OSError as err: bg_log(user, err) res_out( 12, f'Failed to save session file in /home/{user}/tmp: {err}', user, ) try: os.chmod(cookie_file, 0o600) os.chmod(session_url_file, 0o600) except OSError as oserr: bg_log(user, oserr) res_out( 13, f'Failed to set permissions on user files in /home/{user}/tmp', user, ) print('Success') def check_valid_user(user: str): """Checks given user across passwd and /home""" try: homedir = pwd.getpwnam(user)[5] user_id = pwd.getpwnam(user)[2] except KeyError: res_out(3, f'User {user} does not exist on server', user) if not os.path.lexists(homedir): res_out(6, f'User homedir: {homedir} does not exist', user) if not os.path.isdir(homedir): res_out(5, f'User homedir: {homedir} is not a directory', user) if user_id < 500: res_out(4, 'Specified user id below 500', user) def get_user_from_url(url: str) -> str: """Parses username for session, returns username""" try: user = url.split('=')[1].split(':')[0].split('%')[0] return user except IndexError: res_out(2, f"Invalid url: {url}", user) def get_cookie_name(user: str) -> str: """Returns the appropriate cookie location""" user_hash_name = define_userhash(user) cookie_name = f"/home/{user}/tmp/{user_hash_name}_bg_sess" return cookie_name def get_url_file_name(user: str) -> str: """Returns the URL file for the cookie session""" user_hash_name = define_userhash(user) cookie_name = f"/home/{user}/tmp/{user_hash_name}_bg_url" return cookie_name def get_soft_log_file_name(user: str) -> str: """Returns the name of the log file for user""" user_hash_name = define_userhash(user) soft_log_name = f"/home/{user}/tmp/{user_hash_name}_bg_log" return soft_log_name def define_userhash(user: str) -> str: """Returns first 24 characters of the md5 hash of user name""" try: user_hash_name = hashlib.md5(user).hexdigest()[0:23] except IndexError: user_hash_name = user return user_hash_name def load_session(user: str) -> requests.Session: """Returns the full session with the cookie loaded in""" check_valid_user(user) bg_s = requests.Session() cookie_file = get_cookie_name(user) try: with open(cookie_file, encoding='utf-8') as file: cookie = json.load(file) except OSError as exc: bg_log(user, exc) res_out(14, f'Unable to read session cookie file for {user}', user) try: bg_s.cookies = requests.utils.cookiejar_from_dict(cookie) return bg_s except ValueError as exc: bg_log(user, exc) res_out(15, f'Unable to load session cookie for {user}', user) def get_url_from_file(user: str) -> str: "Retrieves individual user's stored session URL" session_url_file = get_url_file_name(user) try: with open(session_url_file, encoding='utf-8') as file: val = json.load(file) except OSError as exc: bg_log(user, exc) val = None else: if isinstance(val, str): return val bg_log(user, repr(val)) res_out(16, f'Unable to read URL session file for {user}', user) def get_user_session_url(url: str, user: str) -> str: """Parses out user session info from the URL""" try: ses = url.split('/')[0:4] except IndexError: res_out(10, f'Incomplete cp_session url: {url}', user) ses = '/'.join(ses) if "cpsess" not in ses: bg_log(user, ses) res_out(11, f'Invalid cp_session url: {url}', user) return ses def delete_file(given_file: str) -> bool: """Deletes provided absolute path of file""" try: if os.path.lexists(given_file): if not os.path.isfile(given_file): return False os.remove(given_file) except OSError: return False return True def delete_session_files(user: str) -> None: """Deletes session files - cookie and url""" url_file = get_url_file_name(user) cookie_file = get_cookie_name(user) if not delete_file(url_file) and not delete_file(cookie_file): bg_log(user, f"ER17: Failed to delete session files for {user}", True) def touch_pid(action: str, user: str, state: Union[str, None] = None) -> None: """This makes sure we lock in our action""" pid_file = f"/home/{user}/tmp/{action}.pid" pid = str(os.getpid()) with open(pid_file, 'w', encoding='utf-8') as pidf: if state: pidf.write(state) else: pidf.write(pid) os.chmod(pid_file, 0o600) def remove_pid(action: str, user: str) -> None: pid_file = f"/home/{user}/tmp/{action}.pid" delete_file(pid_file) def check_min(pid_file: str) -> bool: if not os.path.isfile(pid_file): return False use_by = time.time() - 3 * 60 return os.path.getmtime(pid_file) > use_by def check_pid(action: str, user: str) -> None: pid_file = f"/home/{user}/tmp/{action}.pid" if not check_min(pid_file): return with open(pid_file, encoding='utf-8') as file: content = file.read() if 'Success' in content: # We are going to assume that since the request for action is in the # last 5 minutes, it's a duplicate request, and wants to know current # last state. remove_pid(action, user) print("Success") sys.exit(0) res_out(99, f"{pid_file} for {action} already exists for {user}", user) def initialize_logging(user: str) -> None: """Clears previous log if any""" user_soft_logfile = get_soft_log_file_name(user) date = datetime.now().strftime('%Y-%m-%d-%H-%M-%S') with open(user_soft_logfile, 'a', encoding='utf-8') as file: file.write(f"{date}\n") os.chmod(user_soft_logfile, 0o600) def get_user_theme(user: str) -> Union[str, None]: """gets theme using /var/cpanel/users/$user file""" path = f'/var/cpanel/users/{user}' try: with open(path, encoding='utf-8') as file: data = file.read() except OSError: data = '' if match := re.search("RS=([a-z_0-9]+)", data): return match.group(1) bg_log(user, f"ER99: Unable to determine theme from {path}") return None # -#-# DOCUMENT ROOT CHECK #-#-# def check_document_root(user: str, cp_domain: str) -> None: """Gets and checks the doc root of the domain for conflicting files""" initialize_logging(user) docroot_dirs = check_addon_domains(user) session_domain_docroot = doc_root_call(user, cp_domain) if session_domain_docroot is None: bg_log(user, cp_domain) res_out(27, 'Failed to retrieve document root of domain', user) install_pid_file = f"/home/{user}/tmp/install.pid" if check_min(install_pid_file): return None return file_check(cp_domain, session_domain_docroot, docroot_dirs, user) def check_addon_domains(user: str) -> list[str]: """Pulls addon and sub domains and returns the list""" sub_params = { 'cpanel_jsonapi_user': user, 'cpanel_jsonapi_apiversion': 2, 'cpanel_jsonapi_module': 'SubDomain', 'cpanel_jsonapi_func': 'listsubdomains', } add_params = { 'cpanel_jsonapi_user': user, 'cpanel_jsonapi_apiversion': 2, 'cpanel_jsonapi_module': 'AddonDomain', 'cpanel_jsonapi_func': 'listaddondomains', } main_domain = get_main_domain(user) main_domain_docroot = doc_root_call(user, main_domain) cdr_sess = load_session(user) cdr_user_ses_url = get_url_from_file(user) url = f"{cdr_user_ses_url}/json-api/cpanel" try: res1_res = cdr_sess.get(url, params=sub_params).json() res2_res = cdr_sess.get(url, params=add_params).json() except (requests.RequestException, ValueError) as exc: bg_log(user, exc) res_out( 20, "Unable to gather data from API call. " f"Reinitialize the session. {exc}", user, ) unique = set() unique.update(get_list_of(res1_res)) unique.update(get_list_of(res2_res)) unique.add(main_domain_docroot) return list(unique) def get_main_domain(user: str) -> str: """Gets the document root of main domain""" cdr_sess = load_session(user) cdr_user_ses_url = get_url_from_file(user) cp_params = { 'cpanel_jsonapi_user': user, 'cpanel_jsonapi_apiversion': 2, 'cpanel_jsonapi_module': 'DomainLookup', 'cpanel_jsonapi_func': 'getmaindomain', } url = f"{cdr_user_ses_url}/json-api/cpanel" try: api_res = cdr_sess.get(url, params=cp_params).json() except (requests.RequestException, ValueError) as exc: bg_log(user, exc) res_out( 21, "Unable to gather data from API call. " f"Reinitialize the session. {exc}", user, ) try: main_domain = api_res['cpanelresult']['data'][0]['main_domain'] except (IndexError, KeyError, TypeError) as exc: bg_log(user, exc) res_out(23, 'Bad session data. Please re-initialize session.', user) if main_domain is None: bg_log(user, api_res) res_out(22, 'Failed to retrieve document root of main domain', user) return main_domain def doc_root_call(user: str, cp_domain: str) -> str: """Gets the document root of a domain""" cdr_sess = load_session(user) cdr_user_ses_url = get_url_from_file(user) params = { 'cpanel_jsonapi_apiversion': 2, 'cpanel_jsonapi_module': 'DomainLookup', 'cpanel_jsonapi_func': 'getdocroot', 'domain': cp_domain, } dom_url = f"{cdr_user_ses_url}/execute/DomainInfo/list_domains" api_url = f"{cdr_user_ses_url}/json-api/cpanel" try: api_res = cdr_sess.get(api_url, params=params).json() dom_res = cdr_sess.get(dom_url).json() except (requests.RequestException, ValueError) as exc: bg_log(user, exc) res_out( 26, "Unable to gather data from API call. " f"Reinitialize the session. {exc}", user, ) found = False for key in ('addon_domains', 'sub_domains', 'parked_domains'): for dom in dom_res['data'][key]: if cp_domain == dom: found = True break if dom_res['data']['main_domain'] == cp_domain: found = True if not found: bg_log(user, dom_res) res_out(25, 'Domain not in user cPanel', user) try: session_domain_docroot = api_res['cpanelresult']['data'][0]['docroot'] if session_domain_docroot is None: bg_log(user, api_res) res_out(27, 'Failed to retrieve document root of domain.', user) return session_domain_docroot except (IndexError, KeyError, TypeError) as exc: bg_log(user, exc) res_out(28, 'Bad session data. Re-initialize session.', user) def get_list_of(result_dict: dict) -> list[str]: """Returns list of installations""" try: res_list = result_dict['cpanelresult']['data'] return [x['dir'] for x in res_list] except (KeyError, TypeError): print(result_dict) res_out(24, 'Unable to parse domain docroot dict') return [] def file_check( session_domain: str, session_domain_docroot: str, docroot_dirs: list[str], user: str, ) -> Union[str, None]: """Main function of the document root check""" file_list = get_file_list(session_domain_docroot) if not file_list: return None for docroot_dir in docroot_dirs: if session_domain_docroot in docroot_dir: dirname = docroot_dir.split('/')[-1] if dirname in file_list: file_list.remove(dirname) if compare_files(file_list, user): return None return suggest_bg_dir(file_list, session_domain) def get_file_list(directory: str) -> list[str]: """This returns a list of files in a directory, treat 0 files like the dir doesn't exist, we'll overwrite/create it anyway""" try: if not os.path.lexists(directory): return [] if not os.path.isdir(directory): res_out(29, f'{directory} is not a directory') return os.listdir(directory) except OSError as exc: res_out(30, (f'Unable to list {directory} because of {exc}')) def compare_files(file_list: list[str], user: str) -> bool: """compares listing of a directory to our default file names""" our_files = { '.ftpquota', '.well-known', 'cgi-bin', 'robots.txt', 'favicon.ico', 'phpinfo.php', 'default.htm', 'default.php', 'under_construction.html', '.htaccess', '404.shtml', 'logo-imh.svg', } for file in file_list: if file not in our_files: bg_log(user, f"Bad File: {file}") return False return True def suggest_bg_dir(file_list: list[str], domain: str) -> str: 'If the test fails, suggest bg_domain' bgdirname = f"bg_{domain}" if bgdirname not in file_list: return bgdirname num = 0 bgdirname = f"bg{num}_{domain}" while bgdirname in file_list: num += 1 bgdirname = f"bg{num}_{domain}" return bgdirname # -#-# WORDPRESS INSTALLATION #-#-# def install_wp( user: str, domain: str, wp_admin_username: str, wp_admin_password: str, wp_admin_email: str, bg_license: str, temp_url: Literal[0, 1], wp_softdir: Union[str, None] = None, ): "Makes an api call to softaculous to install WP and BG with provided info." initialize_logging(user) check_pid('install', user) cdr_sess = load_session(user) sess_url = get_url_from_file(user) touch_pid('install', user) user_theme = get_user_theme(user) soft_id = '10001' # bgrid if not check_if_soft(user, soft_id): soft_id = '26' # wp if not check_if_soft(user, soft_id): res_out(100, 'BGrid and WP both missing from Softaculous', user) url = f'{sess_url}/frontend/{user_theme}/softaculous/index.live.php' params = {'api': 'json', 'act': 'software', 'soft': soft_id} main_domain = get_main_domain(user) main_domain_docroot = doc_root_call(user, main_domain) if wp_softdir == 'None': wp_softdir = '' data = set_wp_install_info( domain, wp_admin_username, wp_admin_password, wp_admin_email, wp_softdir, ) try: inst_result = cdr_sess.post(url, params=params, data=data).json() except ValueError: res_out(40, 'cPanel API returned no content. Re-initialize session.') if inst_result is None: res_out(41, 'cPanel API returned no content. Re-initialize session.') if 'error' in inst_result: if isinstance(inst_result["error"], dict): del data["dbuserpass"] del data["admin_pass"] bg_log(user, data) error = ''.join(list(inst_result['error'].values())) else: del data["dbuserpass"] del data["admin_pass"] bg_log(user, data) error = ''.join(inst_result["error"]) res_out(43, error, user) try: dbname = inst_result["__settings"]["softdb"] dbpass = inst_result["__settings"]["softdbpass"] dbuser = inst_result["__settings"]["softdbuser"] wpdir = inst_result["__settings"]["softpath"] soft_url = inst_result["__settings"]["softurl"] except KeyError as kerr: bg_log(user, inst_result) bg_log(user, f"KeyError: {kerr}") res_out(44, 'Softaculous returned no results, check log', user) try: if temp_url == 1: insid = pull_insid(user, wpdir) host_domain = platform.node() new_url = "//" + host_domain + '/~' + user dom_trp = soft_url.split(':')[1] if main_domain_docroot not in wpdir: bg_log(user, main_domain_docroot) res_out( 51, 'Primary domain docroot not in installation path', user ) if main_domain_docroot != wpdir: dir_add = wpdir.rsplit(main_domain_docroot)[1] new_url = new_url + dir_add full_new_url = "http://" + domain + dir_add else: full_new_url = "http://" + domain search_replace(user, wpdir, dom_trp, new_url) # This is the part where we ignore tempURL for softaculous # or it fails, while still updating wpdir update_softaculous( user, insid, wpdir, full_new_url, dbname, dbuser, dbpass ) except Exception as exc: res_out(45, exc, user) bg_license = hashlib.md5(bytes(bg_license, 'ascii')).hexdigest() try: install_bg(dbuser, dbname, dbpass, wpdir, bg_license, soft_id) except Exception as exc: res_out(45, exc, user) curl_purge_if_any(user, domain, wp_softdir, wpdir) touch_pid('install', user, state='Success') if SET_TEST == 0: delete_session_files(user) print("Success") def check_if_soft(user: str, soft_id: str) -> bool: try: sess = load_session(user) sess_url = get_url_from_file(user) user_theme = get_user_theme(user) url = f"{sess_url}/frontend/{user_theme}/softaculous/index.live.php" params = {"api": "json", "act": "home"} inst_result = sess.get(url, params=params).json() return str(soft_id) in json.dumps(inst_result) except ValueError: res_out(46, "Softaculous software query returned bad result", user) def curl_purge_if_any( user: str, domain: str, bgdir: Union[str, None], directory: str ) -> None: """curls url/purge/dir if any""" try: if bgdir: url = f"http://{domain}/purge/{bgdir}/" else: url = f"http://{domain}/purge/" requests.get(url, headers={'Host': domain}, timeout=30.0) except requests.ConnectionError as req_exc: bg_log(user, f"Error: Does DNS exist for {domain}") bg_log(user, req_exc) except requests.RequestException as req_exc: bg_log(user, req_exc) wp_kwargs = {'cwd': directory, 'log_user': user} try: wp_cmd("transient", "delete-all", **wp_kwargs) wp_cmd("cron", "event", "run", "wp_version_check", **wp_kwargs) except (OSError, CalledProcessError) as exc: bg_log(user, exc) bg_log( user, f"Failed to delete transient and version check in {directory}. " "Run Manually", ) def set_wp_install_info( domain: str, wp_admin_username: str, wp_admin_password: str, wp_admin_email: str, wp_softdir: Union[str, None] = None, ) -> dict: 'gets the actual wp installation info, returns dict' wp_softsubmit = '1' chars = string.digits + string.ascii_letters wp_softdb = "".join([random.choice(chars) for i in range(7)]) wp_dbuserpass = "".join([secrets.choice(chars) for i in range(13)]) wp_hostname = 'localhost' wp_language = 'en' wp_site_name = 'WordPress Site' wp_site_desc = 'My Blog' wp_overwrite = '1' wp_installation = { 'softsubmit': wp_softsubmit, 'softdomain': domain, 'softdb': wp_softdb, 'dbuserpass': wp_dbuserpass, 'hostname': wp_hostname, 'admin_username': wp_admin_username, 'admin_pass': wp_admin_password, 'admin_email': wp_admin_email, 'language': wp_language, 'site_name': wp_site_name, 'site_desc': wp_site_desc, 'overwrite_existing': wp_overwrite, 'noemail': 1, } if wp_softdir is not None and wp_softdir != '': wp_installation['softdirectory'] = wp_softdir return wp_installation def install_bg( uname: str, dbname: str, dbpw: str, inst_dir: str, bg_license: str, soft_id: str, ) -> None: user = inst_dir.split('/')[2] plugin_urls = [ 'https://repo.boldgrid.com/boldgrid-inspirations.zip', 'https://downloads.wordpress.org/plugin/post-and-page-builder.zip', 'https://repo.boldgrid.com/boldgrid-connect.zip', ] theme_url = 'https://repo.boldgrid.com/themes/boldgrid-gridone.zip' content_url = 'https://repo.boldgrid.com/assets/boldgrid-default.html' # os.chdir(inst_dir) timestamp = int(time.time()) check_handler(inst_dir) if soft_id == '26': for url in plugin_urls: install_plugin(inst_dir, url) elif soft_id == '10001': for url in plugin_urls: if 'boldgrid-connect.zip' in url: install_plugin(inst_dir, url) db_creds = { 'host': 'localhost', 'user': uname, 'password': dbpw, 'database': dbname, } try: with pymysql.connect(**db_creds) as conn, conn.cursor() as cur: cur.execute( """ INSERT INTO `wp_options` (`option_id`, `option_name`, `option_value`, `autoload`) VALUES(NULL, 'boldgrid_api_key', %s, 'yes') """, (bg_license,), ) conn.commit() cur.execute( """ INSERT INTO `wp_options` (`option_id`, `option_name`, `option_value`, `autoload`) VALUES (NULL, '_transient_timeout_boldgrid_valid_api_key', %s, 'no') """, (timestamp,), ) conn.commit() except pymysql.Error as err: bg_log(user, err) res_out(48, f'Failed to insert BG API data via MySQL: {err}', user) db_creds = { 'host': 'localhost', 'user': uname, 'password': dbpw, 'database': dbname, } wp_kwargs = {'cwd': inst_dir, 'log_user': user} try: wp_cmd("theme", "install", theme_url, "--activate", **wp_kwargs) sess = requests.Session() req = sess.get(content_url) wp_cmd( 'post', 'create', '--post_type=page', '--post_title=WEBSITE COMING SOON', '--post_status=publish', '--comment_status=closed', f'--post_content={req.content}', **wp_kwargs, ) with pymysql.connect(**db_creds) as conn, conn.cursor() as cur: cur.execute( """ SELECT ID FROM wp_posts WHERE post_title LIKE 'WEBSITE COMING SOON' LIMIT 1 """ ) try: post_id = int(cur.fetchone()[0]) except IndexError: res_out(48, 'Failed to get page id for default home page', user) cur.execute( """ UPDATE wp_options SET option_value="MY SITE TITLE" WHERE option_name="blogname" """ ) cur.execute( """ UPDATE wp_options SET option_value="My Tagline" WHERE option_name="blogdescription" """ ) conn.commit() wp_cmd('option', 'update', 'page_on_front', post_id, **wp_kwargs) wp_cmd('option', 'update', 'show_on_front', 'page', **wp_kwargs) with pymysql.connect(**db_creds) as conn, conn.cursor() as cur: cur.execute( """ INSERT INTO `wp_postmeta` (`post_id`, `meta_key`, `meta_value`) VALUES(%s, 'boldgrid_hide_page_title', '0') """, (post_id,), ) conn.commit() except CalledProcessError as exc: res_out(49, f'Failed to install/update the default theme: {exc}', user) except pymysql.Error as exc: res_out(48, f'Failed to insert BG API data via MySQL: {exc}', user) except OSError as exc: res_out(76, f'Failed to backup database prior to move: {exc}', user) def install_plugin(wpdir: str, plugin_zip_url: str) -> None: """Downloads and installs, activates the plugin from the URL provided""" plugin_name = plugin_zip_url.split('/')[-1:][0] dest_zip = os.path.join(wpdir, plugin_name) user = wpdir.split('/')[2] try: with requests.get(plugin_zip_url, stream=True, timeout=30.0) as req: req.raise_for_status() with open(dest_zip, 'wb') as file: # Download and write in 8 KiB chunks (limiting mem usage) for chunk in req.iter_content(chunk_size=8192): file.write(chunk) except requests.RequestException as exc: bg_log( user, f"ERROR 44: Unable to download {plugin_name} " f"plugin from {plugin_zip_url} due to {exc}. Continuing", True, ) except OSError as exc: bg_log(user, exc) bg_log(user, f"ERROR 45: Failed to install plugin: {plugin_name}", True) wp_kwargs = {'cwd': wpdir, 'log_user': user} try: if os.path.isfile(dest_zip): wp_cmd("plugin", "install", "--activate", dest_zip, **wp_kwargs) else: bg_log(user, dest_zip) bg_log( user, f"ERROR 46: Unable to download {plugin_name} " f"plugin from {plugin_zip_url}. Continuing", True, ) except (CalledProcessError, OSError) as exc: bg_log( user, f"ERROR 47: failed to install plugin {plugin_name}: {exc}", True, ) @overload def pull_insid( user: str, bgdir: str, optional: Literal[True] ) -> Union[str, None]: ... @overload def pull_insid(user: str, bgdir: str, optional: Literal[False] = False) -> str: ... def pull_insid(user: str, bgdir: str, optional: bool = False): """Queries softDB for WP db info""" installs = [] wp_lists = check_installations(user) for wordpress, data in wp_lists.items(): wordpress: str if wordpress.startswith('26_') or wordpress.startswith('10001_'): if data['softpath'] == bgdir: installs.append(data) if not installs: if optional: return None bg_log(user, wp_lists) res_out( 50, 'No installations with the directory provided found in SoftDB.', user, ) elif len(installs) == 1: return installs[0]['insid'] itime_count = [] for install in installs: itime_count.append(install['itime']) itime_count.sort() intended_install_time = itime_count[-1] for install in installs: if install['itime'] == intended_install_time: intended_installation = install return intended_installation['insid'] def update_softaculous( user: str, insid: str, new_dir: str, new_url: str, db_name: str, db_user: str, db_pass: str, ): "This function updates the softaculous entry" sess = load_session(user) user_theme = get_user_theme(user) sess_url = get_url_from_file(user) url = f"{sess_url}/frontend/{user_theme}/softaculous/index.live.php" params = {'api': 'json', 'act': 'editdetail', 'insid': insid} payload = { 'editins': '1', 'edit_dir': new_dir, 'edit_url': new_url, 'edit_dbname': db_name, 'edit_dbuser': db_user, 'edit_dbpass': db_pass, 'edit_dbhost': 'localhost', 'noemail': 1, } data = sess.post(url, params=params, data=payload).json() for key in ('fatal_error_text', 'error'): if key in data: bg_log(user, payload) bg_log(user, data[key], True) # -#-# LIST INSTALLATION #-#-# def print_wp_list(user: str, staging: int = 1) -> None: """Prints out a list of WP domains and docroots""" wp_dict = {} for soft_id, data in check_installations(user).items(): soft_id: str if not soft_id.startswith('26_') and not soft_id.startswith('10001_'): continue if staging == 0: wp_dict[get_site_url(user, data['softpath'])] = data['softpath'] continue if re.match(r".*/bg\d*_", data['softpath']): # Forced bg_ staging wp_dict[get_site_url(user, data['softpath'])] = data['softpath'] elif re.match(".*.temporary.link", data['softurl']): # IMH wp_dict[get_site_url(user, data['softpath'])] = data['softpath'] elif re.match(".*.tempsite.link", data['softurl']): # HUB wp_dict[get_site_url(user, data['softpath'])] = data['softpath'] elif re.match(f".*/~{user}", get_site_url(user, data['softpath'])): # TempURL wp_dict[get_site_url(user, data['softpath'])] = data['softpath'] print(json.dumps(wp_dict, indent=4)) def get_site_url(user: str, install_dir: str) -> str: try: ret = wp_cmd( f"--path={install_dir}", 'option', 'list', '--search=siteurl', '--field=option_value', cwd=install_dir, log_user=user, encoding='utf-8', ) except (OSError, CalledProcessError) as exc: bg_log( user, f"ERROR: Failed to run get site_url from {install_dir}: {exc}" ) return None return ret.stdout.strip() def check_installations(user: str) -> dict: "This runs softaculous api call to get a list of installations" soft_dict = {} cdr_sess = load_session(user) cdr_user_ses_url = get_url_from_file(user) user_theme = get_user_theme(user) params = {'api': 'json', 'act': 'installations'} url = f"{cdr_user_ses_url}/frontend/{user_theme}/softaculous/index.live.php" res = cdr_sess.get(url, params=params) if "Login Attempt Failed" in res.content: res_out(60, 'Failed to Login to API. Re-initialize session') inst_result = json.loads(res.content) if inst_result is None: res_out(61, 'cPanel API returned no content. Re-initialize session.') soft_lists = inst_result['installations'] if 'error' in inst_result: error = inst_result["error"] if isinstance(error, dict): err = ''.join(list(error.values())) else: err = ''.join(error) res_out(63, err, user) for installation in soft_lists: for soft_id, install_info in soft_lists[installation].items(): if soft_id.startswith('26_') or soft_id.startswith('10001_'): soft_dict[soft_id] = install_info return soft_dict # -#-# ACTIVATION #-#-# def ready_to_move(bgdir: str, domain: str, ssl: Literal[0, 1]) -> None: "This will move content one directory up" user = bgdir.split('/')[2] check_pid('launch', user) touch_pid('launch', user) initialize_logging(user) try: if bgdir.split('/')[1] != 'home': res_out(70, f'Not the server home path: {bgdir}', user) except IndexError: res_out( 71, 'The absolute path was not provided or does not ' f'contain the "home" directory: {bgdir}', user, ) if not os.path.exists(bgdir): res_out(72, f'{bgdir} does not exist', user) if not os.path.isdir(bgdir): res_out(73, f'{bgdir} is not a directory', user) if len(domain) < 3: res_out(74, f"Domain too short or empty: {domain}", user) full_path_filenames = [] files_to_be_moved = [] docroots = check_addon_domains(user) if bgdir in docroots: predir = bgdir move = 0 else: predir = os.path.split(bgdir)[0] move = 1 try: db_name, db_user, db_pass, db_url, insid = pull_db_data(user, bgdir) db_creds = { 'host': 'localhost', 'user': db_user, 'password': db_pass, 'database': db_name, } with pymysql.connect(**db_creds) as conn, conn.cursor() as cur: cur.execute( """ SELECT option_value FROM wp_options WHERE option_name='siteurl' LIMIT 1 """ ) db_url = str(cur.fetchone()[0]) new_url = '/'.join(db_url.split('/')[:-1]) match_url = db_url.split('//')[-1] match_domain = platform.node() + '/~' + user if match_domain in match_url: new_url = domain elif domain in match_url: new_url = domain else: new_url = '/'.join(db_url.split('/')[:-1]) if new_url == db_url: bg_log(user, db_url) res_out( 82, 'The installation already appears to be for this domain.', user, ) backup_db(user, db_user, db_pass, db_name, match_url, bgdir, predir) except Exception as exc: res_out(45, exc, user) if move == 1: files_in_predir = os.listdir(predir) old_dir_name = os.path.join(predir, suggest_olddir(files_in_predir)) try: os.makedirs(old_dir_name) except OSError as err: bg_log(user, err) bg_log(user, 'ER74: Could not create old site directory.') res_out(74, 'Could not create old site directory.') for filename in files_in_predir: if not re.match(r'old_site.*', filename): file_name = os.path.join(predir, filename) full_path_filenames.append(file_name) for a_dc in docroots: if a_dc in full_path_filenames: full_path_filenames.remove(a_dc) if bgdir in full_path_filenames: full_path_filenames.remove(bgdir) for f_list in full_path_filenames: shutil.move(f_list, old_dir_name) for j_list in os.listdir(bgdir): file_name = os.path.join(bgdir, j_list) files_to_be_moved.append(file_name) for z_list in files_to_be_moved: shutil.move(z_list, predir) try: os.rmdir(bgdir) except OSError as exc: bg_log(user, exc) bg_log(user, f'ER79: Unable to delete {bgdir}', True) try: update_old_softaculous_entry(user, predir, old_dir_name) except Exception as exc: res_out(45, exc, user) try: search_replace(user, predir, bgdir, predir) search_replace(user, predir, match_url, new_url) update_softaculous( user, insid, predir, new_url, db_name, db_user, db_pass ) update_htaccess(predir, user) if ssl == 1: convert_current_to_https(user, predir) except Exception as exc: res_out(45, exc, user) curl_purge_if_any(user, domain, None, predir) touch_pid('launch', user, state='Success') if SET_TEST == 0: delete_session_files(user) print("Success") def update_old_softaculous_entry(user: str, predir: str, olddir: str) -> None: insid = pull_insid(user, predir, optional=True) if not insid: return db_name, db_user, db_pass, db_url, insid = pull_db_data(user, predir) old_db_url = db_url + "/" + str(olddir.split('/')[-1]) backup_db(user, db_user, db_pass, db_name, db_url, predir, olddir) update_softaculous( user, insid, olddir, old_db_url, db_name, db_user, db_pass ) def suggest_olddir(file_list: list[str]) -> str: """Suggests the name of the old_site directory if already exists""" bgdirname = 'old_site' regdirname = 'old_site' if bgdirname not in file_list: return bgdirname num = 0 bgdirname = f"{regdirname}_{num}" while bgdirname in file_list: num += 1 bgdirname = f"{regdirname}_{num}" return bgdirname def pull_db_data(user: str, bgdir: str) -> tuple[str, str, str, str, str]: """Queries softDB for WP db info""" installs = [] wp_lists = check_installations(user) for soft_id, data in wp_lists.items(): soft_id: str if soft_id.startswith('26_') or soft_id.startswith('10001_'): if data['softpath'] == bgdir: installs.append(data) if not installs: bg_log(user, wp_lists) res_out( 75, 'No installations with the directory provided found in SoftDB', user, ) if len(installs) == 1: found = installs[0] else: itime_count = [] for install in installs: itime_count.append(install['itime']) itime_count.sort() intended_install_time = itime_count[-1] for install in installs: if install['itime'] == intended_install_time: found = install db_name = found['softdb'] db_user = found['softdbuser'] db_pass = found['softdbpass'] db_url = found['softurl'] insid = found['insid'] return db_name, db_user, db_pass, db_url, insid def backup_db( user: str, dbuser: str, dbpass: str, dbname: str, old_url: str, bg_dir: str, dump_dir: str, ) -> None: """Backup the db""" date = datetime.now().strftime('%Y-%m-%d-%H-%M-%S') user = bg_dir.split('/')[2] if old_url.startswith('http'): domain = urlparse(old_url)[1] elif old_url.startswith('//'): domain = urlparse(old_url)[1] else: domain = urlparse(old_url)[2].split('/')[0] path = os.path.join(dump_dir, f"{domain}-{dbname}-{date}.sql") try: mysqldump(dbuser, dbpass, dbname, path) except (CalledProcessError, OSError) as exc: res_out(76, f'Failed to backup database prior to move: {exc}', user) def mysqldump(dbuser: str, dbpass: str, dbname: str, path: str) -> None: with open(path, 'wb') as file: run( ["/usr/bin/mysqldump", f"--user={dbuser}", dbname], env={'MYSQL_PWD': dbpass}, stdout=file, encoding='utf-8', errors='ignore', stderr=PIPE, check=True, ) def convert_current_to_https(user: str, path: str) -> bool: "Will find current site URL and search-replace to https" current_site_url = get_site_url(user, path) ssl_site_url = re.sub(r'http', r'https', current_site_url) return search_replace(user, path, current_site_url, ssl_site_url) def wp_cmd( *args: str, cwd: str, log_user: Union[str, None] = None, stdout=DEVNULL, stderr=DEVNULL, check: bool = True, **kwargs, ) -> CompletedProcess: """Runs the wp command""" if log_user: bg_log(log_user, f"Running /usr/local/bin/wp {shlex.join(args)}") return run( ["/usr/local/bin/wp", *args], stdout=stdout, stderr=stderr, cwd=cwd, check=check, **kwargs, ) def update_htaccess(predir: str, user: str) -> None: """Updates htaccess file after moving site""" wp_kwargs = {'log_user': user, 'cwd': predir} try: wp_cmd("rewrite", "flush", **wp_kwargs) wp_yml = os.path.join(predir, "wp-cli.yml") with open(wp_yml, 'w', encoding='utf-8') as file: file.write("apache_modules:\n - mod_rewrite\n") wp_cmd("rewrite", "structure", "/%postname%/", "--hard", **wp_kwargs) except (OSError, CalledProcessError) as exc: bg_log(user, exc) bg_log(user, 'ER81 The .htaccess conversion to new location failed') res_out(81, 'The .htaccess conversion to new location failed') def search_replace(user: str, basedir: str, old_url: str, new_url: str) -> bool: """Updates database to new URL after moving site""" try: wp_cmd( f"--path={basedir}", 'search-replace', old_url, new_url, '--all-tables', log_user=user, cwd=basedir, ) except (OSError, CalledProcessError) as exc: bg_log(user, f"ERROR: Failed to run wp-cli search-replace: {exc}") return False bg_log(user, f"Ran wp-cli search-replace from {old_url!r} --> {new_url!r}") return True def check_handler(inst_dir: str) -> None: """Inserts proper add handler if php version =/< 5.2""" server_php = "AddHandler application/x-httpd-php56 .php" php_re = re.compile(r'^AddHandler\sapplication\/x-httpd-php5[2-4]\s\.php') htaccfile = os.path.join(inst_dir, '.htaccess') userhome_ht = os.path.join( os.path.join(os.path.sep, *inst_dir.split('/')[1:3]), '.htaccess' ) if not os.path.isfile(userhome_ht): return try: with open(userhome_ht, encoding='utf-8') as file: data = file.read().splitlines() edit = False for line in data: if php_re.search(line): edit = True break if not edit: return if not os.path.exists(htaccfile): with open(htaccfile, 'a', encoding='utf-8'): os.utime(htaccfile, None) with open(htaccfile, 'r+', encoding='utf-8') as file: content = file.read() file.seek(0, 0) file.write(server_php.rstrip('\r\n') + '\n' + content) except OSError as exc: sys.exit(f".htaccess convert failed: {exc}") def delete_installation(installdir: str) -> None: """This removes the provided installation if exists in softdb""" user = installdir.split('/')[2] check_pid('delete', user) touch_pid('delete', user) check_valid_user(user) if not os.path.isdir(installdir): res_out(85, f"{installdir} does not exist.", user) cdr_sess = load_session(user) sess_url = get_url_from_file(user) user_theme = get_user_theme(user) dbname, dbuser, dbpass, _, insid = pull_db_data(user, installdir) date = datetime.now().strftime('%Y-%m-%d-%H-%M-%S') path = os.path.join(installdir, f"{insid}-{dbname}-{date}_predelete.sql") try: mysqldump(dbuser, dbpass, dbname, path) except (CalledProcessError, OSError) as exc: res_out(86, f'Failed to backup database prior to deletion: {exc}', user) url = f"{sess_url}/frontend/{user_theme}/softaculous/index.live.php" params = {'noemail': 1, 'api': 'json', 'act': 'remove', 'insid': insid} data = { 'removeins': '1', 'remove_db': '1', 'remove_dbuser': '1', 'noemail': '1', } inst_result = cdr_sess.post(url, params=params, data=data).json() dirname = installdir.split('/')[-1] if not 'public_html' in dirname: # We are not moving things manually out of public_html predir = os.path.split(installdir)[0] renamed_dir = f"{dirname}_deleted" num = 0 file_list = get_file_list(predir) while renamed_dir in file_list: num = num + 1 renamed_dir = f"{dirname}_deleted_{num}" os.rename(installdir, renamed_dir) if 'old_site' not in dirname: os.makedirs(installdir) if inst_result['done'] is not True: bg_log(user, inst_result) res_out(84, 'Unable to remove installation. Check logs', user) if SET_TEST == 0: delete_session_files(user) touch_pid('delete', user, state='Success') print("Success") def res_out(code: int, msg, log_user: Union[str, None] = None) -> NoReturn: """Exits with given error numerical for documentation""" print(f"ERROR {code}: {msg}") if log_user: bg_log(log_user, f'ER{code}: {msg}') sys.exit(code) def bg_log(user: str, log_content, show: bool = False): "logs/appends given info to to /home/user5/tmp" if show: print(log_content) user_soft_logfile = get_soft_log_file_name(user) try: serialized = json.dumps(log_content, indent=4) except (TypeError, ValueError): serialized = pprint.pformat(log_content) try: with open(user_soft_logfile, 'a', encoding='utf-8') as file: file.write(f"{serialized}\n") except OSError as exc: print(f"ERROR 90: Unable to log result. {exc}. Continuing.") def main(): "Main funct of BG_API" args = parse_call() if args.funct == "initialize": initialize_session(args.URL) elif args.funct == "check_doc_root": check_document_root(args.cp_user, args.cp_domain) elif args.funct == "install": install_wp( args.cpuser, args.cpdomain, args.wp_uname, args.wp_pass, args.wp_email, args.bglicense, args.temp_url, args.wp_softdir, ) elif args.funct == "list": print_wp_list(args.bg_user, args.staging) elif args.funct == "launch": ready_to_move(args.bg_directory, args.domain, args.ssl) elif args.funct == "delete": delete_installation(args.bg_installation) else: res_out(1, "Bad Arguments") if __name__ == '__main__': main()