mirror of
https://github.com/aaPanel/aaPanel.git
synced 2026-08-17 21:25:47 +02:00
708 lines
26 KiB
Python
708 lines
26 KiB
Python
#!/usr/bin/env btpython
|
|
"""
|
|
WordPress Helper - Site creation and WP-CLI management.
|
|
Usage: btpython wp_helper.py <command> [JSON args]
|
|
"""
|
|
import os
|
|
import sys
|
|
import json
|
|
import subprocess
|
|
import sqlite3
|
|
import time
|
|
import re
|
|
import shutil
|
|
import random
|
|
import string
|
|
import traceback
|
|
import logging
|
|
from pathlib import Path
|
|
from typing import Dict, List, Optional, Tuple, Any
|
|
|
|
# ========== Configuration ==========
|
|
PANEL_PATH = '/www/server/panel'
|
|
PANEL_DB = os.path.join(PANEL_PATH, 'data/default.db')
|
|
WP_CLI_PATH = '/usr/local/bin/wp'
|
|
VHOST_NGINX = os.path.join(PANEL_PATH, 'vhost/nginx')
|
|
VHOST_APACHE = os.path.join(PANEL_PATH, 'vhost/apache')
|
|
VHOST_OLS = os.path.join(PANEL_PATH, 'vhost/openlitespeed')
|
|
VHOST_OLS_DETAIL = os.path.join(PANEL_PATH, 'vhost/openlitespeed/detail')
|
|
VHOST_REWRITE = os.path.join(PANEL_PATH, 'vhost/rewrite')
|
|
WWWLOGS = '/www/wwwlogs'
|
|
|
|
# Logging
|
|
logging.basicConfig(
|
|
level=logging.INFO,
|
|
format='%(asctime)s [%(levelname)s] %(message)s',
|
|
handlers=[logging.StreamHandler(sys.stderr)]
|
|
)
|
|
logger = logging.getLogger('wp_helper')
|
|
|
|
|
|
# ========== Output ==========
|
|
def json_out(data: Dict[str, Any]) -> None:
|
|
"""Output JSON to stdout."""
|
|
print(json.dumps(data, ensure_ascii=False, default=str))
|
|
|
|
|
|
def run_command(commands: Dict[str, callable]) -> None:
|
|
"""Main command dispatcher."""
|
|
if len(sys.argv) < 2:
|
|
print(f"Usage: btpython wp_helper.py <command> [JSON args]")
|
|
print(f"Available: {', '.join(commands.keys())}")
|
|
sys.exit(1)
|
|
cmd = sys.argv[1]
|
|
if cmd not in commands:
|
|
json_out({"status": False, "msg": f"Unknown command: {cmd}, available: {', '.join(commands.keys())}"})
|
|
sys.exit(1)
|
|
args: Dict[str, Any] = {}
|
|
if len(sys.argv) > 2:
|
|
try:
|
|
args = json.loads(sys.argv[2])
|
|
except json.JSONDecodeError:
|
|
json_out({"status": False, "msg": f"JSON parse error: {sys.argv[2]}"})
|
|
sys.exit(1)
|
|
if not isinstance(args, dict):
|
|
json_out({"status": False, "msg": f"Invalid args type: expected dict, got {type(args).__name__}"})
|
|
sys.exit(1)
|
|
try:
|
|
logger.info(f"Executing command: {cmd}")
|
|
json_out(commands[cmd](args))
|
|
except Exception as e:
|
|
logger.error(f"Command {cmd} failed: {e}\n{traceback.format_exc()}")
|
|
json_out({"status": False, "msg": f"Execution error: {str(e)}", "traceback": traceback.format_exc()})
|
|
sys.exit(1)
|
|
|
|
|
|
# ========== Database ==========
|
|
def get_panel_db() -> sqlite3.Connection:
|
|
"""Get panel database connection."""
|
|
conn = sqlite3.connect(PANEL_DB)
|
|
conn.row_factory = sqlite3.Row
|
|
return conn
|
|
|
|
|
|
def db_query(sql: str, params: Tuple = (), fetchone: bool = False) -> Any:
|
|
"""Execute SELECT query."""
|
|
conn = get_panel_db()
|
|
try:
|
|
cur = conn.execute(sql, params)
|
|
if fetchone:
|
|
row = cur.fetchone()
|
|
return dict(row) if row else None
|
|
return [dict(r) for r in cur.fetchall()]
|
|
finally:
|
|
conn.close()
|
|
|
|
|
|
def db_execute(sql: str, params: Tuple = ()) -> int:
|
|
"""Execute INSERT/UPDATE/DELETE query. Returns last rowid."""
|
|
conn = get_panel_db()
|
|
try:
|
|
conn.execute(sql, params)
|
|
conn.commit()
|
|
return conn.execute('SELECT last_insert_rowid()').fetchone()[0]
|
|
finally:
|
|
conn.close()
|
|
|
|
|
|
def get_mysql_root_password() -> str:
|
|
"""Get MySQL root password from panel config."""
|
|
row = db_query('SELECT mysql_root FROM config WHERE id=1', fetchone=True)
|
|
return (row.get('mysql_root', '') if row else '') or ''
|
|
|
|
|
|
def get_mysql_bin() -> str:
|
|
"""Find MySQL binary path."""
|
|
for p in ['/www/server/mysql/bin/mysql', '/usr/bin/mysql', '/usr/local/bin/mysql']:
|
|
if os.path.exists(p):
|
|
return p
|
|
return 'mysql'
|
|
|
|
|
|
def run_mysql_sql(sql: str, root_password: Optional[str] = None) -> Tuple[bool, str]:
|
|
"""Execute MySQL SQL statement."""
|
|
if root_password is None:
|
|
root_password = get_mysql_root_password()
|
|
mysql_bin = get_mysql_bin()
|
|
try:
|
|
result = subprocess.run(
|
|
[mysql_bin, '-uroot', f'-p{root_password}', '-e', sql],
|
|
capture_output=True, text=True, timeout=30
|
|
)
|
|
return result.returncode == 0, result.stderr.strip()
|
|
except Exception as e:
|
|
return False, str(e)
|
|
|
|
|
|
# ========== Common Utilities ==========
|
|
def get_php_bin(version: str) -> str:
|
|
"""Get PHP binary path for version."""
|
|
return f'/www/server/php/{version}/bin/php'
|
|
|
|
|
|
def detect_php_version() -> Optional[str]:
|
|
"""Detect highest installed PHP version."""
|
|
php_base = "/www/server/php"
|
|
if not os.path.exists(php_base):
|
|
return None
|
|
versions = []
|
|
for d in os.listdir(php_base):
|
|
if d.isdigit() and os.path.exists(os.path.join(php_base, d, 'bin/php')):
|
|
versions.append(d)
|
|
return max(versions) if versions else None
|
|
|
|
|
|
def get_date() -> str:
|
|
"""Get current datetime string."""
|
|
return time.strftime('%Y-%m-%d %X')
|
|
|
|
|
|
def run_cmd(cmd: List[str], timeout: int = 60) -> Tuple[bool, str, str]:
|
|
"""Execute command safely (no shell=True)."""
|
|
try:
|
|
result = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout)
|
|
return result.returncode == 0, result.stdout.strip(), result.stderr.strip()
|
|
except subprocess.TimeoutExpired:
|
|
return False, '', f'Command timed out after {timeout}s'
|
|
except Exception as e:
|
|
return False, '', str(e)
|
|
|
|
|
|
def shell(cmd: str, timeout: int = 60) -> Tuple[bool, str, str]:
|
|
"""Execute shell command (for simple operations only)."""
|
|
try:
|
|
result = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=timeout)
|
|
return result.returncode == 0, result.stdout.strip(), result.stderr.strip()
|
|
except subprocess.TimeoutExpired:
|
|
return False, '', f'Command timed out after {timeout}s'
|
|
except Exception as e:
|
|
return False, '', str(e)
|
|
|
|
|
|
def write_file(path: str, content: str, mode: str = 'w') -> None:
|
|
"""Write content to file, creating directories if needed."""
|
|
os.makedirs(os.path.dirname(path), exist_ok=True)
|
|
with open(path, mode) as f:
|
|
f.write(content)
|
|
|
|
|
|
def is_multi_webservice() -> bool:
|
|
"""Check if multiple web services are installed."""
|
|
return (os.path.exists('/www/server/nginx/sbin/nginx')
|
|
and os.path.exists('/www/server/apache/bin/apachectl')
|
|
and os.path.exists('/usr/local/lsws/bin/lswsctrl'))
|
|
|
|
|
|
def resolve_site(s_id: int) -> Tuple[Optional[Dict], Optional[str]]:
|
|
"""Resolve site info and PHP version from site ID."""
|
|
try:
|
|
s_id = int(s_id)
|
|
except (ValueError, TypeError):
|
|
return None, None
|
|
site = db_query('SELECT id,path,name FROM sites WHERE id=?', (s_id,), fetchone=True)
|
|
if not site:
|
|
return None, None
|
|
conf_path = os.path.join(VHOST_NGINX, f'{site["name"]}.conf')
|
|
php_ver = None
|
|
if os.path.exists(conf_path):
|
|
with open(conf_path, 'r') as f:
|
|
m = re.search(r'enable-php-(\d+)\.conf', f.read())
|
|
if m:
|
|
php_ver = m.group(1)
|
|
if not php_ver:
|
|
php_ver = detect_php_version()
|
|
return site, php_ver
|
|
|
|
|
|
# ========== WP-CLI ==========
|
|
def ensure_wp_cli() -> bool:
|
|
"""Check and install WP-CLI if not present."""
|
|
if os.path.exists(WP_CLI_PATH):
|
|
return True
|
|
try:
|
|
logger.info("Installing WP-CLI...")
|
|
subprocess.run(
|
|
['curl', '-sSL', '-o', WP_CLI_PATH,
|
|
'https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar'],
|
|
capture_output=True, timeout=120
|
|
)
|
|
os.chmod(WP_CLI_PATH, 0o755)
|
|
return os.path.exists(WP_CLI_PATH)
|
|
except Exception as e:
|
|
logger.error(f"WP-CLI installation failed: {e}")
|
|
return False
|
|
|
|
|
|
def run_wp_cli(args: List[str], php_version: str, cwd: Optional[str] = None) -> Tuple[bool, str, str]:
|
|
"""Execute WP-CLI command."""
|
|
php_bin = get_php_bin(php_version)
|
|
cmd = [php_bin, '-d', 'memory_limit=512M', WP_CLI_PATH, '--allow-root'] + args
|
|
if cwd:
|
|
cmd += ['--path=' + cwd]
|
|
return run_cmd(cmd, timeout=300)
|
|
|
|
|
|
# ========== Web Config Generation ==========
|
|
def gen_nginx_conf(domain: str, php_version: str, site_path: str) -> str:
|
|
"""Generate Nginx virtual host configuration."""
|
|
rewrite_conf = os.path.join(VHOST_REWRITE, domain + '.conf')
|
|
return f'''server
|
|
{{
|
|
listen 80;
|
|
listen [::]:80;
|
|
server_name {domain};
|
|
index index.php index.html index.htm default.php default.htm default.html;
|
|
root {site_path};
|
|
|
|
#SSL-START
|
|
#error_page 404/404.html;
|
|
#SSL-END
|
|
|
|
#ERROR-PAGE-START
|
|
error_page 404 /404.html;
|
|
error_page 502 /502.html;
|
|
#ERROR-PAGE-END
|
|
|
|
#PHP-INFO-START
|
|
include enable-php-{php_version}.conf;
|
|
#PHP-INFO-END
|
|
|
|
#REWRITE-START
|
|
include {rewrite_conf};
|
|
#REWRITE-END
|
|
|
|
location ~ ^/(\\.user\\.ini|\\.htaccess|\\.git|\\.env|\\.svn|\\.project|LICENSE|README.md)
|
|
{{
|
|
return 404;
|
|
}}
|
|
|
|
location ~ \\.well-known{{
|
|
allow all;
|
|
}}
|
|
|
|
if ( $uri ~ "^/\\.well-known/.*\\.(php|jsp|py|js|css|lua|ts|go|zip|tar\\.gz|rar|7z|sql|bak)$" ) {{
|
|
return 403;
|
|
}}
|
|
|
|
location ~ .*\\.(gif|jpg|jpeg|png|bmp|swf)$
|
|
{{
|
|
expires 30d;
|
|
error_log /dev/null;
|
|
access_log /dev/null;
|
|
}}
|
|
|
|
location ~ .*\\.(js|css)?$
|
|
{{
|
|
expires 12h;
|
|
error_log /dev/null;
|
|
access_log /dev/null;
|
|
}}
|
|
access_log {WWWLOGS}/{domain}.log;
|
|
error_log {WWWLOGS}/{domain}.error.log;
|
|
}}'''
|
|
|
|
|
|
def gen_apache_conf(domain: str, php_version: str, site_path: str, port: str = '80') -> str:
|
|
"""Generate Apache virtual host configuration."""
|
|
php_proxy = f'unix:/tmp/php-cgi-{php_version}.sock|fcgi://localhost'
|
|
return f'''<VirtualHost *:{port}>
|
|
ServerAdmin webmaster@example.com
|
|
DocumentRoot "{site_path}"
|
|
ServerName {time.strftime('%Y%m%d%H%M%S')}.{domain}
|
|
ServerAlias {domain}
|
|
#errorDocument 404 /404.html
|
|
ErrorLog "{WWWLOGS}/{domain}-error_log"
|
|
CustomLog "{WWWLOGS}/{domain}-access_log" combined
|
|
|
|
#DENY FILES
|
|
<Files ~ (\\.user\\.ini|\\.htaccess|\\.git|\\.env|\\.svn|\\.project|LICENSE|README.md)$>
|
|
Order allow,deny
|
|
Deny from all
|
|
</Files>
|
|
|
|
#PHP
|
|
<FilesMatch \\.php$>
|
|
SetHandler "proxy:{php_proxy}"
|
|
</FilesMatch>
|
|
|
|
#PATH
|
|
<Directory "{site_path}">
|
|
SetOutputFilter DEFLATE
|
|
Options FollowSymLinks
|
|
AllowOverride All
|
|
Require all granted
|
|
DirectoryIndex index.php index.html index.htm default.php default.html default.htm
|
|
</Directory>
|
|
</VirtualHost>'''
|
|
|
|
|
|
def gen_ols_conf(domain: str, php_version: str, site_path: str) -> Tuple[str, str]:
|
|
"""Generate OpenLiteSpeed configuration."""
|
|
if not site_path.endswith('/'):
|
|
site_path += '/'
|
|
main_conf = f"""
|
|
#VHOST {domain} START
|
|
virtualhost {domain} {{
|
|
vhRoot {site_path}
|
|
configFile {PANEL_PATH}/vhost/openlitespeed/detail/{domain}.conf
|
|
allowSymbolLink 1
|
|
enableScript 1
|
|
restrained 1
|
|
setUIDMode 0
|
|
}}
|
|
#VHOST {domain} END
|
|
"""
|
|
detail_conf = f'''docRoot $VH_ROOT
|
|
vhDomain $VH_NAME
|
|
adminEmails example@example.com
|
|
enableGzip 1
|
|
enableIpGeo 1
|
|
|
|
index {{
|
|
useServer 0
|
|
indexFiles index.php,index.html
|
|
}}
|
|
|
|
errorlog /www/wwwlogs/$VH_NAME_ols.error_log {{
|
|
useServer 0
|
|
logLevel ERROR
|
|
rollingSize 10M
|
|
}}
|
|
|
|
accesslog /www/wwwlogs/$VH_NAME_ols.access_log {{
|
|
useServer 0
|
|
logFormat '%{{X-Forwarded-For}}i %h %l %u %t "%r" %>s %b "%{{Referer}}i" "%{{User-Agent}}i"'
|
|
logHeaders 5
|
|
rollingSize 10M
|
|
keepDays 10 compressArchive 1
|
|
}}
|
|
|
|
scripthandler {{
|
|
add lsapi:{domain} php
|
|
}}
|
|
|
|
extprocessor {domain} {{
|
|
type lsapi
|
|
address UDS://tmp/lshttpd/{domain}.sock
|
|
maxConns 300
|
|
env LSAPI_CHILDREN=300
|
|
env LSAPI_AVOID_FORK=1
|
|
initTimeout 600
|
|
retryTimeout 5
|
|
persistConn 1
|
|
pcKeepAliveTimeout 30
|
|
respBuffer 0
|
|
autoStart 1
|
|
path /usr/local/lsws/lsphp{php_version}/bin/lsphp
|
|
extUser www
|
|
extGroup www
|
|
memSoftLimit 2047M
|
|
memHardLimit 2047M
|
|
procSoftLimit 1000
|
|
procHardLimit 1100
|
|
}}
|
|
|
|
phpIniOverride {{
|
|
php_admin_value open_basedir "/tmp/:{site_path}"
|
|
}}
|
|
|
|
expires {{
|
|
enableExpires 1
|
|
expiresByType image/*=A43200,text/css=A43200,application/x-javascript=A43200,application/javascript=A43200,font/*=A43200,application/x-font-ttf=A43200
|
|
}}
|
|
|
|
rewrite {{
|
|
enable 1
|
|
autoLoadHtaccess 1
|
|
include {PANEL_PATH}/vhost/openlitespeed/proxy/{domain}/urlrewrite/*.conf
|
|
include {PANEL_PATH}/vhost/apache/redirect/{domain}/*.conf
|
|
include {PANEL_PATH}/vhost/openlitespeed/redirect/{domain}/*.conf
|
|
}}
|
|
include {PANEL_PATH}/vhost/openlitespeed/proxy/{domain}/*.conf
|
|
'''
|
|
return main_conf, detail_conf
|
|
|
|
|
|
def write_wordpress_rewrite(domain: str) -> None:
|
|
"""Write WordPress rewrite rules."""
|
|
rewrite_path = os.path.join(VHOST_REWRITE, domain + '.conf')
|
|
with open(rewrite_path, 'w') as f:
|
|
f.write("location /\n{\n try_files $uri $uri/ /index.php?$args;\n}\n\n"
|
|
"rewrite /wp-admin$ $scheme://$host$uri/ permanent;\n")
|
|
|
|
|
|
def ensure_ols_listen_conf(domain: str) -> None:
|
|
"""Ensure OpenLiteSpeed listen configuration includes domain."""
|
|
listen_dir = os.path.join(PANEL_PATH, 'vhost', 'openlitespeed', 'listen')
|
|
os.makedirs(listen_dir, exist_ok=True)
|
|
listen_file = os.path.join(listen_dir, '80.conf')
|
|
listen_conf = ''
|
|
if os.path.exists(listen_file):
|
|
with open(listen_file, 'r') as f:
|
|
listen_conf = f.read()
|
|
map_line = f'\tmap\t{domain} {domain}'
|
|
if listen_conf:
|
|
rep = r'map\s+{0}.*'.format(re.escape(domain))
|
|
if not re.search(rep, listen_conf):
|
|
listen_conf = re.sub(r'secure\s*0', 'secure 0\n' + map_line, listen_conf)
|
|
else:
|
|
listen_conf = f"\nlistener Default80{{\n address *:80\n secure 0\n{map_line}\n}}\n"
|
|
with open(listen_file, 'w') as f:
|
|
f.write(listen_conf)
|
|
|
|
|
|
def write_all_web_configs(domain: str, php_version: str, site_path: str) -> None:
|
|
"""Write all web server configurations (Nginx, Apache, OLS)."""
|
|
multi = is_multi_webservice()
|
|
apache_port = '8288' if multi else '80'
|
|
write_file(os.path.join(VHOST_NGINX, domain + '.conf'), gen_nginx_conf(domain, php_version, site_path))
|
|
write_file(os.path.join(VHOST_APACHE, domain + '.conf'), gen_apache_conf(domain, php_version, site_path, port=apache_port))
|
|
htaccess = os.path.join(site_path, '.htaccess')
|
|
if not os.path.exists(htaccess):
|
|
write_file(htaccess, ' ')
|
|
shell(f'chmod 644 {htaccess}')
|
|
shell(f'chown www:www {htaccess}')
|
|
ols_main, ols_detail = gen_ols_conf(domain, php_version, site_path)
|
|
write_file(os.path.join(VHOST_OLS, domain + '.conf'), ols_main, mode='a+')
|
|
write_file(os.path.join(VHOST_OLS_DETAIL, domain + '.conf'), ols_detail)
|
|
ensure_ols_listen_conf(domain)
|
|
write_wordpress_rewrite(domain)
|
|
ok, _, _ = shell('nginx -t')
|
|
if ok:
|
|
shell('nginx -s reload')
|
|
|
|
|
|
# ========== Site Cleanup ==========
|
|
def _cleanup_site(s_id: int, d_id: int, domain: str, db_name: str, db_user: str, site_path: str) -> None:
|
|
"""Clean up site resources on failure."""
|
|
for fn, args in [
|
|
(db_execute, ('DELETE FROM domain WHERE pid=?', (s_id,))),
|
|
(db_execute, ('DELETE FROM wordpress_onekey WHERE s_id=?', (s_id,))),
|
|
(db_execute, ('DELETE FROM databases WHERE id=?', (d_id,))),
|
|
(db_execute, ('DELETE FROM sites WHERE id=?', (s_id,))),
|
|
]:
|
|
try:
|
|
fn(*args)
|
|
except Exception as e:
|
|
logger.warning(f"Cleanup query failed: {e}")
|
|
run_mysql_sql(
|
|
f"DROP DATABASE IF EXISTS `{db_name}`; DROP USER IF EXISTS `{db_user}`@'localhost'; FLUSH PRIVILEGES;",
|
|
get_mysql_root_password()
|
|
)
|
|
if os.path.exists(site_path):
|
|
shutil.rmtree(site_path, ignore_errors=True)
|
|
for p in [
|
|
os.path.join(VHOST_NGINX, domain + '.conf'),
|
|
os.path.join(VHOST_APACHE, domain + '.conf'),
|
|
os.path.join(VHOST_OLS_DETAIL, domain + '.conf'),
|
|
os.path.join(VHOST_REWRITE, domain + '.conf'),
|
|
]:
|
|
if os.path.exists(p):
|
|
os.remove(p)
|
|
ols_main = os.path.join(VHOST_OLS, domain + '.conf')
|
|
if os.path.exists(ols_main):
|
|
try:
|
|
content = open(ols_main, 'r').read()
|
|
content = re.sub(f'#VHOST {domain} START.*?#VHOST {domain} END\n?', '', content, flags=re.DOTALL)
|
|
open(ols_main, 'w').write(content)
|
|
except Exception as e:
|
|
logger.warning(f"OLS cleanup failed: {e}")
|
|
shell('nginx -s reload')
|
|
|
|
|
|
# ========== Commands ==========
|
|
|
|
def cmd_create_site(args: Dict[str, Any]) -> Dict[str, Any]:
|
|
"""Create a new WordPress site."""
|
|
required = ['weblog_title', 'user_name', 'admin_email', 'admin_password']
|
|
for key in required:
|
|
if key not in args or not args[key]:
|
|
return {"status": False, "msg": f"Missing required parameter: {key}"}
|
|
domain = args.get('domain', 'example.com')
|
|
if not isinstance(domain, str):
|
|
domain = str(domain)
|
|
domain = domain.strip().lower()
|
|
if not domain:
|
|
domain = 'example.com'
|
|
password = args['admin_password']
|
|
if not isinstance(password, str):
|
|
password = str(password)
|
|
if len(password) < 8:
|
|
return {"status": False, "msg": "Password must be at least 8 characters"}
|
|
existing = db_query('SELECT id FROM sites WHERE name=?', (domain,), fetchone=True)
|
|
if existing:
|
|
return {"status": False, "msg": f"Site {domain} already exists (ID: {existing['id']})"}
|
|
if not os.path.exists("/www/server/mysql"):
|
|
return {"status": False, "msg": "MySQL is not installed. Please install MySQL first."}
|
|
if not os.path.exists("/www/server/php"):
|
|
return {"status": False, "msg": "PHP is not installed. Please install PHP first."}
|
|
php_version = args.get('php_version', '')
|
|
if not isinstance(php_version, str):
|
|
php_version = str(php_version) if php_version else ''
|
|
php_version = php_version.strip()
|
|
if not php_version:
|
|
php_version = detect_php_version()
|
|
if not php_version:
|
|
return {"status": False, "msg": "No installed PHP version detected"}
|
|
if not os.path.exists(f"/www/server/php/{php_version}"):
|
|
available = [d for d in os.listdir("/www/server/php/") if d.isdigit()]
|
|
return {"status": False, "msg": f"PHP {php_version} not installed. Available: {', '.join(sorted(available))}"}
|
|
if not ensure_wp_cli():
|
|
return {"status": False, "msg": "WP-CLI download failed. Please install manually to /usr/local/bin/wp"}
|
|
|
|
site_path = f"/www/wwwroot/{domain}"
|
|
db_name = 'wp_' + ''.join(random.choices(string.ascii_lowercase + string.digits, k=8))
|
|
db_user = db_name
|
|
db_password = password
|
|
now = get_date()
|
|
prefix = args.get('prefix', 'wp_')
|
|
s_id = d_id = None
|
|
|
|
try:
|
|
os.makedirs(site_path, exist_ok=True)
|
|
ok, err = run_mysql_sql(
|
|
f"CREATE DATABASE IF NOT EXISTS `{db_name}` CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; "
|
|
f"CREATE USER `{db_user}`@'localhost' IDENTIFIED BY '{db_password}'; "
|
|
f"GRANT ALL PRIVILEGES ON `{db_name}`.* TO `{db_user}`@'localhost'; FLUSH PRIVILEGES;"
|
|
)
|
|
if not ok:
|
|
return {"status": False, "msg": f"Failed to create database: {err}"}
|
|
|
|
d_id = db_execute(
|
|
'INSERT INTO databases (pid,sid,db_type,name,username,password,accept,type,ps,addtime) VALUES (?,?,?,?,?,?,?,?,?,?)',
|
|
(0, 0, 0, db_name, db_user, db_password, '127.0.0.1', 'MySQL', domain, now)
|
|
)
|
|
s_id = db_execute(
|
|
'INSERT INTO sites (name,path,status,ps,type_id,addtime,project_type,project_config) VALUES (?,?,?,?,?,?,?,?)',
|
|
(domain, site_path, '1', args.get('ps', domain), 0, now, 'WP2', '{}')
|
|
)
|
|
db_execute('UPDATE databases SET pid=? WHERE id=?', (s_id, d_id))
|
|
db_execute('INSERT INTO domain (pid,name,port,addtime) VALUES (?,?,?,?)', (s_id, domain, '80', now))
|
|
|
|
ok, out, err = run_wp_cli(['core', 'download', '--locale=' + args.get('language', 'en_US'), '--force'], php_version, cwd=site_path)
|
|
if not ok:
|
|
raise Exception(f"WordPress download failed: {err or out}")
|
|
ok, out, err = run_wp_cli([
|
|
'config', 'create', f'--dbname={db_name}', f'--dbuser={db_user}',
|
|
f'--dbpass={db_password}', '--dbhost=localhost', f'--dbprefix={prefix}', '--force',
|
|
], php_version, cwd=site_path)
|
|
if not ok:
|
|
raise Exception(f"wp-config.php creation failed: {err or out}")
|
|
title = args['weblog_title']
|
|
admin_user = args['user_name']
|
|
admin_email = args['admin_email']
|
|
ok, out, err = run_wp_cli([
|
|
'core', 'install', f'--url=http://{domain}', f'--title={title}',
|
|
f'--admin_user={admin_user}', f'--admin_password={password}',
|
|
f'--admin_email={admin_email}', '--skip-email'
|
|
], php_version, cwd=site_path)
|
|
if not ok:
|
|
raise Exception(f"WordPress installation failed: {err or out}")
|
|
|
|
write_all_web_configs(domain, php_version, site_path)
|
|
shell(f'chown -R www:www {site_path}')
|
|
shell(f'chmod -R 755 {site_path}')
|
|
db_execute(
|
|
'INSERT INTO wordpress_onekey (s_id,d_id,prefix,user,pass,site_type) VALUES (?,?,?,?,?,?)',
|
|
(s_id, d_id, prefix, args['user_name'], password, 'Default category')
|
|
)
|
|
logger.info(f"WordPress site created: {domain} (ID: {s_id})")
|
|
return {
|
|
"status": True, "msg": "WordPress site created successfully", "domain": domain,
|
|
"path": site_path, "s_id": s_id, "d_id": d_id, "database": db_name,
|
|
"admin_url": f"http://{domain}/wp-admin", "admin_user": args['user_name'],
|
|
}
|
|
except Exception as e:
|
|
if s_id is not None:
|
|
_cleanup_site(s_id, d_id or 0, domain, db_name, db_user, site_path)
|
|
return {"status": False, "msg": f"Deployment failed: {str(e)}"}
|
|
|
|
|
|
def cmd_site_info(args: Dict[str, Any]) -> Dict[str, Any]:
|
|
"""Get site details by domain or s_id."""
|
|
domain = args.get('domain', '')
|
|
if not isinstance(domain, str):
|
|
domain = str(domain) if domain else ''
|
|
domain = domain.strip().lower()
|
|
s_id = args.get('s_id')
|
|
if s_id is not None:
|
|
try:
|
|
s_id = int(s_id)
|
|
except (ValueError, TypeError):
|
|
return {"status": False, "msg": f"Invalid s_id: {s_id}"}
|
|
site = db_query('SELECT id,path,name,status,ps,project_type FROM sites WHERE id=?', (s_id,), fetchone=True)
|
|
elif domain:
|
|
site = db_query('SELECT id,path,name,status,ps,project_type FROM sites WHERE name=?', (domain,), fetchone=True)
|
|
else:
|
|
return {"status": False, "msg": "Please provide domain or s_id"}
|
|
if not site:
|
|
return {"status": False, "msg": "Site not found"}
|
|
site['database'] = db_query('SELECT name,username,password FROM databases WHERE pid=?', (site['id'],), fetchone=True)
|
|
site['domains'] = db_query('SELECT name,port FROM domain WHERE pid=?', (site['id'],))
|
|
return {"status": True, "data": site}
|
|
|
|
|
|
def cmd_list_sites(args: Dict[str, Any]) -> Dict[str, Any]:
|
|
"""List all WordPress sites."""
|
|
return {"status": True, "data": db_query("SELECT id,name,path,status,ps FROM sites WHERE project_type='WP2'")}
|
|
|
|
|
|
def cmd_ensure_wp_cli(args: Dict[str, Any]) -> Dict[str, Any]:
|
|
"""Check and install WP-CLI if not present."""
|
|
if ensure_wp_cli():
|
|
return {"status": True, "msg": "WP-CLI is ready", "path": WP_CLI_PATH}
|
|
return {"status": False, "msg": "WP-CLI installation failed. Please install manually: curl -o /usr/local/bin/wp https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar && chmod +x /usr/local/bin/wp"}
|
|
|
|
|
|
def cmd_site_wcli(args: Dict[str, Any]) -> Dict[str, Any]:
|
|
"""Get WP-CLI command prefix for a site."""
|
|
s_id = args.get('s_id')
|
|
if s_id is None:
|
|
return {"status": False, "msg": "Please provide s_id"}
|
|
try:
|
|
s_id = int(s_id)
|
|
except (ValueError, TypeError):
|
|
return {"status": False, "msg": f"Invalid s_id: {s_id}"}
|
|
site, php_ver = resolve_site(s_id)
|
|
if not site:
|
|
return {"status": False, "msg": "Site not found"}
|
|
php_bin = get_php_bin(php_ver)
|
|
site_path = site['path']
|
|
prefix = f"{php_bin} -d memory_limit=512M {WP_CLI_PATH} --allow-root --path={site_path}"
|
|
return {
|
|
"status": True, "domain": site['name'], "path": site_path,
|
|
"php_bin": php_bin, "wp_cli": WP_CLI_PATH, "cmd_prefix": prefix,
|
|
}
|
|
|
|
|
|
def cmd_wp_cli(args: Dict[str, Any]) -> Dict[str, Any]:
|
|
"""Execute WP-CLI command for a site."""
|
|
s_id = args.get('s_id')
|
|
wp_args = args.get('args', [])
|
|
if s_id is None:
|
|
return {"status": False, "msg": "Please provide s_id"}
|
|
try:
|
|
s_id = int(s_id)
|
|
except (ValueError, TypeError):
|
|
return {"status": False, "msg": f"Invalid s_id: {s_id}"}
|
|
if not wp_args:
|
|
return {"status": False, "msg": "Please provide args (WP-CLI subcommand argument list)"}
|
|
site, php_ver = resolve_site(s_id)
|
|
if not site:
|
|
return {"status": False, "msg": "Site not found"}
|
|
php_bin = get_php_bin(php_ver)
|
|
site_path = site['path']
|
|
cmd = [php_bin, '-d', 'memory_limit=512M', WP_CLI_PATH, '--allow-root', f'--path={site_path}'] + wp_args
|
|
returncode, stdout, stderr = run_cmd(cmd, timeout=300)
|
|
return {"status": returncode, "stdout": stdout, "stderr": stderr}
|
|
|
|
|
|
# ========== Entry Point ==========
|
|
run_command({
|
|
'create-site': cmd_create_site,
|
|
'site-info': cmd_site_info,
|
|
'list-sites': cmd_list_sites,
|
|
'ensure-wp-cli': cmd_ensure_wp_cli,
|
|
'site-wcli': cmd_site_wcli,
|
|
})
|