Added validation of paths to the info plugin

This commit is contained in:
Kamil Krzyśków 2024-02-29 22:05:44 +01:00 committed by Martin Donath
parent 819e209795
commit 4478522b52
2 changed files with 208 additions and 32 deletions

View File

@ -27,13 +27,14 @@ import requests
import site
import sys
import yaml
from colorama import Fore, Style
from importlib.metadata import distributions, version
from io import BytesIO
from markdown.extensions.toc import slugify
from mkdocs.config.defaults import MkDocsConfig
from mkdocs.plugins import BasePlugin, event_priority
from mkdocs.utils import get_theme_dir
from mkdocs.utils import get_yaml_loader
import regex
from zipfile import ZipFile, ZIP_DEFLATED
@ -97,7 +98,7 @@ class InfoPlugin(BasePlugin[InfoConfig]):
# hack to detect whether the custom_dir setting was used without parsing
# mkdocs.yml again - we check at which position the directory provided
# by the theme resides, and if it's not the first one, abort.
if config.theme.dirs.index(get_theme_dir(config.theme.name)):
if config.theme.custom_dir:
log.error("Please remove 'custom_dir' setting.")
self._help_on_customizations_and_exit()
@ -109,27 +110,57 @@ class InfoPlugin(BasePlugin[InfoConfig]):
log.error("Please remove 'hooks' setting.")
self._help_on_customizations_and_exit()
# Assure that config_file_path is absolute.
# If the --config-file option is used then the path is
# used as provided, so it is likely relative.
if not os.path.isabs(config.config_file_path):
config.config_file_path = os.path.normpath(os.path.join(
os.getcwd(),
config.config_file_path
))
# Assure that possible relative paths, which will be validated
# or used to generate other paths are absolute.
config.config_file_path = _convert_to_abs(config.config_file_path)
config_file_parent = os.path.dirname(config.config_file_path)
# The theme.custom_dir property cannot be set, therefore a helper
# variable is used.
custom_dir = config.theme.custom_dir
if custom_dir:
custom_dir = _convert_to_abs(
custom_dir,
abs_prefix = config_file_parent
)
# Support projects plugin
projects_plugin = config.plugins.get("material/projects")
if projects_plugin:
abs_projects_dir = os.path.normpath(
os.path.join(
os.path.dirname(config.config_file_path),
projects_plugin.config.projects_dir
)
abs_projects_dir = _convert_to_abs(
projects_plugin.config.projects_dir,
abs_prefix = config_file_parent
)
else:
abs_projects_dir = ""
# Load the current MkDocs config(s) to get access to INHERIT
loaded_configs = _load_yaml(config.config_file_path)
if not isinstance(loaded_configs, list):
loaded_configs = [loaded_configs]
# Validate different MkDocs paths to assure that
# they're children of the current working directory.
paths_to_validate = [
config.config_file_path,
config.docs_dir,
custom_dir or "",
abs_projects_dir,
*[cfg.get("INHERIT", "") for cfg in loaded_configs]
]
for hook in config.hooks:
path = _convert_to_abs(hook, abs_prefix = config_file_parent)
paths_to_validate.append(path)
for path in list(paths_to_validate):
if not path or path.startswith(os.getcwd()):
paths_to_validate.remove(path)
if paths_to_validate:
log.error(f"One or more paths aren't children of root")
self._help_on_not_in_cwd(paths_to_validate)
# Create in-memory archive and prompt author for a short descriptive
# name for the archive, which is also used as the directory name. Note
# that the name is slugified for better readability and stripped of any
@ -295,7 +326,28 @@ class InfoPlugin(BasePlugin[InfoConfig]):
if self.config.archive_stop_on_violation:
sys.exit(1)
# Exclude files, which we don't want in our zip file
# Print help on not in current working directory and exit
def _help_on_not_in_cwd(self, bad_paths):
print(Fore.RED)
print(" The current working (root) directory:\n")
print(f" {os.getcwd()}\n")
print(" is not a parent of the following paths:")
print(Style.NORMAL)
for path in bad_paths:
print(f" {path}")
print()
print(" To assure that all project files are found")
print(" please adjust your config or file structure and")
print(" put everything within the root directory of the project.\n")
print(" Please also make sure `mkdocs build` is run in")
print(" the actual root directory of the project.")
print(Style.RESET_ALL)
# Exit, unless explicitly told not to
if self.config.archive_stop_on_violation:
sys.exit(1)
# Exclude files which we don't want in our zip file
def _is_excluded(self, posix_path: str) -> bool:
for pattern in self.exclusion_patterns:
if regex.match(pattern, posix_path):
@ -318,6 +370,42 @@ def _size(value, factor = 1):
return f"{color}{value:3.1f} {unit}"
value /= 1000.0
# To validate if a file is within the file tree,
# it needs to be absolute, so that it is possible to
# check the prefix.
def _convert_to_abs(path: str, abs_prefix: str = None) -> str:
if os.path.isabs(path): return path
if abs_prefix is None: abs_prefix = os.getcwd()
return os.path.normpath(os.path.join(abs_prefix, path))
# Custom YAML loader - required to handle the parent INHERIT config.
# It converts the INHERIT path to absolute as a side effect.
# Returns the loaded config, or a list of all loaded configs.
def _load_yaml(abs_src_path: str):
with open(abs_src_path, "r", encoding ="utf-8-sig") as file:
source = file.read()
try:
result = yaml.load(source, Loader = get_yaml_loader()) or {}
except yaml.YAMLError:
result = {}
if "INHERIT" in result:
relpath = result.get('INHERIT')
parent_path = os.path.dirname(abs_src_path)
abspath = _convert_to_abs(relpath, abs_prefix = parent_path)
if os.path.exists(abspath):
result["INHERIT"] = abspath
log.debug(f"Loading inherited configuration file: {abspath}")
parent = _load_yaml(abspath)
if isinstance(parent, list):
result = [result, *parent]
elif isinstance(parent, dict):
result = [result, parent]
return result
# Load info.gitignore, ignore any empty lines or # comments
def _load_exclusion_patterns(path: str = None):
if path is None:

View File

@ -27,13 +27,14 @@ import requests
import site
import sys
import yaml
from colorama import Fore, Style
from importlib.metadata import distributions, version
from io import BytesIO
from markdown.extensions.toc import slugify
from mkdocs.config.defaults import MkDocsConfig
from mkdocs.plugins import BasePlugin, event_priority
from mkdocs.utils import get_theme_dir
from mkdocs.utils import get_yaml_loader
import regex
from zipfile import ZipFile, ZIP_DEFLATED
@ -97,7 +98,7 @@ class InfoPlugin(BasePlugin[InfoConfig]):
# hack to detect whether the custom_dir setting was used without parsing
# mkdocs.yml again - we check at which position the directory provided
# by the theme resides, and if it's not the first one, abort.
if config.theme.dirs.index(get_theme_dir(config.theme.name)):
if config.theme.custom_dir:
log.error("Please remove 'custom_dir' setting.")
self._help_on_customizations_and_exit()
@ -109,27 +110,57 @@ class InfoPlugin(BasePlugin[InfoConfig]):
log.error("Please remove 'hooks' setting.")
self._help_on_customizations_and_exit()
# Assure that config_file_path is absolute.
# If the --config-file option is used then the path is
# used as provided, so it is likely relative.
if not os.path.isabs(config.config_file_path):
config.config_file_path = os.path.normpath(os.path.join(
os.getcwd(),
config.config_file_path
))
# Assure that possible relative paths, which will be validated
# or used to generate other paths are absolute.
config.config_file_path = _convert_to_abs(config.config_file_path)
config_file_parent = os.path.dirname(config.config_file_path)
# The theme.custom_dir property cannot be set, therefore a helper
# variable is used.
custom_dir = config.theme.custom_dir
if custom_dir:
custom_dir = _convert_to_abs(
custom_dir,
abs_prefix = config_file_parent
)
# Support projects plugin
projects_plugin = config.plugins.get("material/projects")
if projects_plugin:
abs_projects_dir = os.path.normpath(
os.path.join(
os.path.dirname(config.config_file_path),
projects_plugin.config.projects_dir
)
abs_projects_dir = _convert_to_abs(
projects_plugin.config.projects_dir,
abs_prefix = config_file_parent
)
else:
abs_projects_dir = ""
# Load the current MkDocs config(s) to get access to INHERIT
loaded_configs = _load_yaml(config.config_file_path)
if not isinstance(loaded_configs, list):
loaded_configs = [loaded_configs]
# Validate different MkDocs paths to assure that
# they're children of the current working directory.
paths_to_validate = [
config.config_file_path,
config.docs_dir,
custom_dir or "",
abs_projects_dir,
*[cfg.get("INHERIT", "") for cfg in loaded_configs]
]
for hook in config.hooks:
path = _convert_to_abs(hook, abs_prefix = config_file_parent)
paths_to_validate.append(path)
for path in list(paths_to_validate):
if not path or path.startswith(os.getcwd()):
paths_to_validate.remove(path)
if paths_to_validate:
log.error(f"One or more paths aren't children of root")
self._help_on_not_in_cwd(paths_to_validate)
# Create in-memory archive and prompt author for a short descriptive
# name for the archive, which is also used as the directory name. Note
# that the name is slugified for better readability and stripped of any
@ -295,7 +326,28 @@ class InfoPlugin(BasePlugin[InfoConfig]):
if self.config.archive_stop_on_violation:
sys.exit(1)
# Exclude files, which we don't want in our zip file
# Print help on not in current working directory and exit
def _help_on_not_in_cwd(self, bad_paths):
print(Fore.RED)
print(" The current working (root) directory:\n")
print(f" {os.getcwd()}\n")
print(" is not a parent of the following paths:")
print(Style.NORMAL)
for path in bad_paths:
print(f" {path}")
print()
print(" To assure that all project files are found")
print(" please adjust your config or file structure and")
print(" put everything within the root directory of the project.\n")
print(" Please also make sure `mkdocs build` is run in")
print(" the actual root directory of the project.")
print(Style.RESET_ALL)
# Exit, unless explicitly told not to
if self.config.archive_stop_on_violation:
sys.exit(1)
# Exclude files which we don't want in our zip file
def _is_excluded(self, posix_path: str) -> bool:
for pattern in self.exclusion_patterns:
if regex.match(pattern, posix_path):
@ -318,6 +370,42 @@ def _size(value, factor = 1):
return f"{color}{value:3.1f} {unit}"
value /= 1000.0
# To validate if a file is within the file tree,
# it needs to be absolute, so that it is possible to
# check the prefix.
def _convert_to_abs(path: str, abs_prefix: str = None) -> str:
if os.path.isabs(path): return path
if abs_prefix is None: abs_prefix = os.getcwd()
return os.path.normpath(os.path.join(abs_prefix, path))
# Custom YAML loader - required to handle the parent INHERIT config.
# It converts the INHERIT path to absolute as a side effect.
# Returns the loaded config, or a list of all loaded configs.
def _load_yaml(abs_src_path: str):
with open(abs_src_path, "r", encoding ="utf-8-sig") as file:
source = file.read()
try:
result = yaml.load(source, Loader = get_yaml_loader()) or {}
except yaml.YAMLError:
result = {}
if "INHERIT" in result:
relpath = result.get('INHERIT')
parent_path = os.path.dirname(abs_src_path)
abspath = _convert_to_abs(relpath, abs_prefix = parent_path)
if os.path.exists(abspath):
result["INHERIT"] = abspath
log.debug(f"Loading inherited configuration file: {abspath}")
parent = _load_yaml(abspath)
if isinstance(parent, list):
result = [result, *parent]
elif isinstance(parent, dict):
result = [result, parent]
return result
# Load info.gitignore, ignore any empty lines or # comments
def _load_exclusion_patterns(path: str = None):
if path is None: