1
0
mirror of https://github.com/squidfunk/mkdocs-material.git synced 2024-11-12 01:50:52 +01:00

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 site
import sys import sys
import yaml
from colorama import Fore, Style from colorama import Fore, Style
from importlib.metadata import distributions, version from importlib.metadata import distributions, version
from io import BytesIO from io import BytesIO
from markdown.extensions.toc import slugify from markdown.extensions.toc import slugify
from mkdocs.config.defaults import MkDocsConfig from mkdocs.config.defaults import MkDocsConfig
from mkdocs.plugins import BasePlugin, event_priority from mkdocs.plugins import BasePlugin, event_priority
from mkdocs.utils import get_theme_dir from mkdocs.utils import get_yaml_loader
import regex import regex
from zipfile import ZipFile, ZIP_DEFLATED 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 # hack to detect whether the custom_dir setting was used without parsing
# mkdocs.yml again - we check at which position the directory provided # mkdocs.yml again - we check at which position the directory provided
# by the theme resides, and if it's not the first one, abort. # 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.") log.error("Please remove 'custom_dir' setting.")
self._help_on_customizations_and_exit() self._help_on_customizations_and_exit()
@ -109,27 +110,57 @@ class InfoPlugin(BasePlugin[InfoConfig]):
log.error("Please remove 'hooks' setting.") log.error("Please remove 'hooks' setting.")
self._help_on_customizations_and_exit() self._help_on_customizations_and_exit()
# Assure that config_file_path is absolute. # Assure that possible relative paths, which will be validated
# If the --config-file option is used then the path is # or used to generate other paths are absolute.
# used as provided, so it is likely relative. config.config_file_path = _convert_to_abs(config.config_file_path)
if not os.path.isabs(config.config_file_path): config_file_parent = os.path.dirname(config.config_file_path)
config.config_file_path = os.path.normpath(os.path.join(
os.getcwd(), # The theme.custom_dir property cannot be set, therefore a helper
config.config_file_path # 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 # Support projects plugin
projects_plugin = config.plugins.get("material/projects") projects_plugin = config.plugins.get("material/projects")
if projects_plugin: if projects_plugin:
abs_projects_dir = os.path.normpath( abs_projects_dir = _convert_to_abs(
os.path.join( projects_plugin.config.projects_dir,
os.path.dirname(config.config_file_path), abs_prefix = config_file_parent
projects_plugin.config.projects_dir
)
) )
else: else:
abs_projects_dir = "" 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 # Create in-memory archive and prompt author for a short descriptive
# name for the archive, which is also used as the directory name. Note # 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 # 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: if self.config.archive_stop_on_violation:
sys.exit(1) 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: def _is_excluded(self, posix_path: str) -> bool:
for pattern in self.exclusion_patterns: for pattern in self.exclusion_patterns:
if regex.match(pattern, posix_path): if regex.match(pattern, posix_path):
@ -318,6 +370,42 @@ def _size(value, factor = 1):
return f"{color}{value:3.1f} {unit}" return f"{color}{value:3.1f} {unit}"
value /= 1000.0 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 # Load info.gitignore, ignore any empty lines or # comments
def _load_exclusion_patterns(path: str = None): def _load_exclusion_patterns(path: str = None):
if path is None: if path is None:

View File

@ -27,13 +27,14 @@ import requests
import site import site
import sys import sys
import yaml
from colorama import Fore, Style from colorama import Fore, Style
from importlib.metadata import distributions, version from importlib.metadata import distributions, version
from io import BytesIO from io import BytesIO
from markdown.extensions.toc import slugify from markdown.extensions.toc import slugify
from mkdocs.config.defaults import MkDocsConfig from mkdocs.config.defaults import MkDocsConfig
from mkdocs.plugins import BasePlugin, event_priority from mkdocs.plugins import BasePlugin, event_priority
from mkdocs.utils import get_theme_dir from mkdocs.utils import get_yaml_loader
import regex import regex
from zipfile import ZipFile, ZIP_DEFLATED 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 # hack to detect whether the custom_dir setting was used without parsing
# mkdocs.yml again - we check at which position the directory provided # mkdocs.yml again - we check at which position the directory provided
# by the theme resides, and if it's not the first one, abort. # 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.") log.error("Please remove 'custom_dir' setting.")
self._help_on_customizations_and_exit() self._help_on_customizations_and_exit()
@ -109,27 +110,57 @@ class InfoPlugin(BasePlugin[InfoConfig]):
log.error("Please remove 'hooks' setting.") log.error("Please remove 'hooks' setting.")
self._help_on_customizations_and_exit() self._help_on_customizations_and_exit()
# Assure that config_file_path is absolute. # Assure that possible relative paths, which will be validated
# If the --config-file option is used then the path is # or used to generate other paths are absolute.
# used as provided, so it is likely relative. config.config_file_path = _convert_to_abs(config.config_file_path)
if not os.path.isabs(config.config_file_path): config_file_parent = os.path.dirname(config.config_file_path)
config.config_file_path = os.path.normpath(os.path.join(
os.getcwd(), # The theme.custom_dir property cannot be set, therefore a helper
config.config_file_path # 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 # Support projects plugin
projects_plugin = config.plugins.get("material/projects") projects_plugin = config.plugins.get("material/projects")
if projects_plugin: if projects_plugin:
abs_projects_dir = os.path.normpath( abs_projects_dir = _convert_to_abs(
os.path.join( projects_plugin.config.projects_dir,
os.path.dirname(config.config_file_path), abs_prefix = config_file_parent
projects_plugin.config.projects_dir
)
) )
else: else:
abs_projects_dir = "" 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 # Create in-memory archive and prompt author for a short descriptive
# name for the archive, which is also used as the directory name. Note # 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 # 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: if self.config.archive_stop_on_violation:
sys.exit(1) 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: def _is_excluded(self, posix_path: str) -> bool:
for pattern in self.exclusion_patterns: for pattern in self.exclusion_patterns:
if regex.match(pattern, posix_path): if regex.match(pattern, posix_path):
@ -318,6 +370,42 @@ def _size(value, factor = 1):
return f"{color}{value:3.1f} {unit}" return f"{color}{value:3.1f} {unit}"
value /= 1000.0 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 # Load info.gitignore, ignore any empty lines or # comments
def _load_exclusion_patterns(path: str = None): def _load_exclusion_patterns(path: str = None):
if path is None: if path is None: