File: //usr/local/lib/python3.9/site-packages/wordfence/cli/config/ini_parser.py
import errno
import json
import os
from argparse import Namespace
from configparser import ConfigParser, NoSectionError
from typing import List, Set, Any, Callable, Optional
from wordfence.logging import log
from .config_items import Context, ConfigItemDefinition, \
CanonicalValueExtractorInterface, not_set_token, \
ReferenceToken, merge_config_maps
from .defaults import INI_DEFAULT_PATH
from .base_config_definitions import config_map as base_config_map
from ..subcommands import SubcommandDefinition
valid_contexts: Set[Context] = {Context.ALL, Context.CONFIG}
GLOBAL_INI_PATH = b'/etc/wordfence/wordfence-cli.ini'
DEFAULT_SECTION_NAME = 'DEFAULT'
class IniCanonicalValueExtractor(CanonicalValueExtractorInterface):
def __init__(self, *config_section_names):
self.config_section_names = config_section_names
def is_valid_source(self, source: Any) -> bool:
return isinstance(source, ConfigParser)
def _get_value_from_section(
self,
definition: ConfigItemDefinition,
source: ConfigParser,
section: str
) -> Any:
if definition.has_separator():
# always return separated values as a string
return source.get(
section,
definition.property_name,
fallback=not_set_token
)
elif definition.get_value_type() == bool:
return source.getboolean(
section,
definition.property_name,
fallback=not_set_token
)
elif definition.get_value_type() == int:
return source.getint(
section,
definition.property_name,
fallback=not_set_token
)
elif definition.accepts_paths():
path = source.get(
section,
definition.property_name,
fallback=not_set_token
)
if path != not_set_token:
path = os.fsencode(path)
return path
elif isinstance(definition.get_value_type(), Callable):
value = source.get(
section,
definition.property_name,
fallback=not_set_token
)
# convert using the type method
value = value if isinstance(value, ReferenceToken) else (
(definition.get_value_type())(value))
return value
elif definition.get_value_type() != str:
raise ValueError(
"Only string, bool, int, and callable types are currently "
"known to the INI parser"
)
else:
return source.get(
section,
definition.property_name,
fallback=not_set_token
)
def get_canonical_value(self, definition: ConfigItemDefinition,
source: ConfigParser) -> Any:
self.assert_is_valid_source(source)
for section in self.config_section_names:
value = self._get_value_from_section(
definition,
source,
section
)
if value is not None and value is not not_set_token:
break
if isinstance(value, str) and definition.has_separator():
value = value.split(definition.meta.separator)
if definition.get_value_type() == int:
value = [int(string_int) for string_int in value]
elif definition.get_value_type() != str:
raise ValueError(
"INI files currently support lists of strings and ints, no"
" other types")
return value
def get_context(self) -> Context:
return Context.CONFIG
def get_ini_value_extractor(
subcommand_definition: SubcommandDefinition
) -> IniCanonicalValueExtractor:
return IniCanonicalValueExtractor(
subcommand_definition.config_section,
DEFAULT_SECTION_NAME
)
def get_default_ini_value_extractor() -> IniCanonicalValueExtractor:
return IniCanonicalValueExtractor(DEFAULT_SECTION_NAME)
def get_ini_path(cli_values: Namespace) -> str:
if 'configuration' not in cli_values or not isinstance(
cli_values.configuration, bytes):
path = INI_DEFAULT_PATH
else:
path = cli_values.configuration
return os.path.expanduser(path)
def load_ini(
cli_values,
subcommand_definition: Optional[SubcommandDefinition]
) -> (ConfigParser, Optional[str]):
config = ConfigParser()
try:
with open(GLOBAL_INI_PATH, 'r') as file:
config.read_file(file)
except FileNotFoundError:
pass # Ignore nonexistant global config files
except OSError:
log.warning(f'Failed to read global config file at {GLOBAL_INI_PATH}')
ini_path = get_ini_path(cli_values)
try:
with open(ini_path) as file:
config.read_file(file)
except OSError as e:
if e.errno == errno.EACCES:
raise PermissionError(
f"The current user cannot read the config file: "
f"{json.dumps(get_ini_path(cli_values))}") from e
elif e.errno != errno.ENOENT:
raise
# config file does not exist: proceed with default values + CLI values
return (config, None)
section_map = {
DEFAULT_SECTION_NAME: base_config_map,
}
if subcommand_definition is not None:
section_map[subcommand_definition.config_section] = \
merge_config_maps(
base_config_map,
subcommand_definition.get_config_map()
)
all_section_names: List[str] = config.sections()
invalid_settings: bool = False
for section_name in all_section_names:
if section_name not in section_map:
config.remove_section(section_name)
# remove values that are in the incorrect context or are entirely unknown
for section, definitions in section_map.items():
try:
items = config.items(section)
except NoSectionError:
items = {}
for property_name, _value in items:
# arguments are stored in the lookup by name (kebab-case), but
# written out in snake_case in the INI
key = property_name.replace('_', '-')
# detect unknown definitions and definitions written in kebab-case
# instead of snake_case
if key not in definitions or (
key == property_name and '-' in property_name):
log.warning(
"Ignoring unknown config setting "
f"{json.dumps(property_name)}"
)
config.remove_option(section, property_name)
invalid_settings = True
if key in definitions:
valid_ini_value = definitions[key].context in valid_contexts
if not valid_ini_value:
log.warning(
f"Ignoring setting that is not valid in the config "
f"file context: {json.dumps(definitions[key].name)}.")
invalid_settings = True
config.remove_option(section, property_name)
continue
if invalid_settings:
log.warning(
"*** Invalid settings not known to wordfence-cli or that are not "
"intended for use in INI config files were discarded. ***")
return (config, ini_path)