1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198
|
# SPDX-License-Identifier: AGPL-3.0-or-later
"""Configuration helper for WordPress."""
import os
import pathlib
import random
import shutil
import string
import augeas
from plinth import action_utils
from plinth.actions import privileged
_public_access_file = pathlib.Path('/etc/wordpress/is_public')
_config_file_path = pathlib.Path('/etc/wordpress/config-default.php')
_static_config_file_path = pathlib.Path('/etc/wordpress/freedombox-static.php')
_db_file_path = pathlib.Path('/etc/wordpress/database.php')
_db_backup_file = pathlib.Path(
'/var/lib/plinth/backups-data/wordpress-database.sql')
DB_HOST = 'localhost'
DB_NAME = 'wordpress_fbx'
DB_USER = 'wordpress_fbx'
@privileged
def setup():
"""Create initial configuration and database for WordPress."""
if _db_file_path.exists() or _config_file_path.exists():
if _config_file_path.exists():
_upgrade_config_file()
return
db_password = _generate_secret_key(16)
_create_config_file(DB_HOST, DB_NAME, DB_USER, db_password)
with action_utils.service_ensure_running('mysql'):
_create_database(DB_NAME)
_set_privileges(DB_HOST, DB_NAME, DB_USER, db_password)
def _create_config_file(db_host, db_name, db_user, db_password):
"""Create a PHP configuration file included by WordPress."""
secret_keys = [_generate_secret_key() for _ in range(8)]
config_contents = f'''<?php
# Created by FreedomBox
include_once('{_static_config_file_path}');
include_once('{_db_file_path}');
define('DB_NAME', $dbname);
define('DB_USER', $dbuser);
define('DB_PASSWORD', $dbpass);
define('DB_HOST', $dbserver);
define('AUTH_KEY', '{secret_keys[0]}');
define('SECURE_AUTH_KEY', '{secret_keys[1]}');
define('LOGGED_IN_KEY', '{secret_keys[2]}');
define('NONCE_KEY', '{secret_keys[3]}');
define('AUTH_SALT', '{secret_keys[4]}');
define('SECURE_AUTH_SALT', '{secret_keys[5]}');
define('LOGGED_IN_SALT', '{secret_keys[6]}');
define('NONCE_SALT', '{secret_keys[7]}');
define('WP_CONTENT_DIR', '/var/lib/wordpress/wp-content');
define('DISABLE_WP_CRON', true);
'''
_config_file_path.write_text(config_contents, encoding='utf-8')
db_contents = f'''<?php
# Created by FreedomBox
$dbuser='{db_user}';
$dbpass='{db_password}';
$dbname='{db_name}';
$dbserver='{db_host}';
'''
old_umask = os.umask(0o037)
try:
_db_file_path.write_text(db_contents, encoding='utf-8')
finally:
os.umask(old_umask)
shutil.chown(_db_file_path, group='www-data')
def _create_database(db_name):
"""Create an empty MySQL database for WordPress."""
# Wordpress' install.php creates the tables.
# SQL injection is avoided due to known input.
query = f'''CREATE DATABASE {db_name};'''
action_utils.run(['mysql', '--user', 'root'], input=query.encode(),
check=True)
def _set_privileges(db_host, db_name, db_user, db_password):
"""Create user, set password and provide permissions on the database."""
# SQL injection is avoided due to known input.
query = f'''GRANT SELECT,INSERT,UPDATE,DELETE,CREATE,DROP,ALTER
ON {db_name}.*
TO {db_user}@{db_host}
IDENTIFIED BY '{db_password}';
FLUSH PRIVILEGES;
'''
action_utils.run(['mysql', '--user', 'root'], input=query.encode(),
check=True)
def _generate_secret_key(length=64, chars=None):
"""Generate a new random secret key for use with WordPress."""
chars = chars or (string.ascii_letters + string.digits +
'!@#$%^&*()-_ []{}<>~`+=,.:/?|')
rand = random.SystemRandom()
return ''.join(rand.choice(chars) for _ in range(length))
def _upgrade_config_file():
"""Upgrade existing config file to add changes."""
include_line = f"include_once('{_static_config_file_path}');"
lines = _config_file_path.read_text(encoding='utf-8').splitlines()
if include_line not in lines:
lines.insert(2, include_line) # Insert on 3rd line
_config_file_path.write_text('\n'.join(lines), encoding='utf-8')
@privileged
def set_public(enable: bool):
"""Allow/disallow public access."""
if enable:
_public_access_file.touch()
else:
_public_access_file.unlink(missing_ok=True)
action_utils.service_reload('apache2')
def is_public() -> bool:
"""Return whether public access is enabled."""
return _public_access_file.exists()
@privileged
def dump_database():
"""Dump database to file."""
with action_utils.service_ensure_running('mysql'):
with _db_backup_file.open('w', encoding='utf-8') as file_handle:
action_utils.run([
'mysqldump', '--add-drop-database', '--add-drop-table',
'--add-drop-trigger', '--user', 'root', '--databases', DB_NAME
], stdout=file_handle, check=True)
@privileged
def restore_database():
"""Restore database from file."""
with action_utils.service_ensure_running('mysql'):
with _db_backup_file.open('r', encoding='utf-8') as file_handle:
action_utils.run(['mysql', '--user', 'root'], stdin=file_handle,
check=True)
_set_privileges(DB_HOST, DB_NAME, DB_USER, _read_db_password())
def _read_db_password():
"""Return the password stored in the DB configuration file."""
aug = _load_augeas()
return aug.get('./$dbpass').strip('\'"')
def _load_augeas():
"""Initialize augeas."""
aug = augeas.Augeas(flags=augeas.Augeas.NO_LOAD +
augeas.Augeas.NO_MODL_AUTOLOAD)
aug.transform('Phpvars', str(_db_file_path))
aug.set('/augeas/context', '/files' + str(_db_file_path))
aug.load()
return aug
@privileged
def uninstall():
"""Remove config files and drop database."""
_drop_database(DB_HOST, DB_NAME, DB_USER)
for file_ in [_public_access_file, _config_file_path, _db_file_path]:
file_.unlink(missing_ok=True)
def _drop_database(db_host, db_name, db_user):
"""Drop the mysql database that was created during install."""
with action_utils.service_ensure_running('mysql'):
query = f"DROP DATABASE {db_name};"
action_utils.run(['mysql', '--user', 'root'], input=query.encode(),
check=False)
query = f"DROP USER IF EXISTS {db_user}@{db_host};"
action_utils.run(['mysql', '--user', 'root'], input=query.encode(),
check=False)
|