Skip to content

Commit 4478522

Browse files
kamilkrzyskowsquidfunk
authored andcommitted
Added validation of paths to the info plugin
1 parent 819e209 commit 4478522

File tree

2 files changed

+208
-32
lines changed

2 files changed

+208
-32
lines changed

Diff for: material/plugins/info/plugin.py

+104-16
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,14 @@
2727
import site
2828
import sys
2929

30+
import yaml
3031
from colorama import Fore, Style
3132
from importlib.metadata import distributions, version
3233
from io import BytesIO
3334
from markdown.extensions.toc import slugify
3435
from mkdocs.config.defaults import MkDocsConfig
3536
from mkdocs.plugins import BasePlugin, event_priority
36-
from mkdocs.utils import get_theme_dir
37+
from mkdocs.utils import get_yaml_loader
3738
import regex
3839
from zipfile import ZipFile, ZIP_DEFLATED
3940

@@ -97,7 +98,7 @@ def on_config(self, config):
9798
# hack to detect whether the custom_dir setting was used without parsing
9899
# mkdocs.yml again - we check at which position the directory provided
99100
# by the theme resides, and if it's not the first one, abort.
100-
if config.theme.dirs.index(get_theme_dir(config.theme.name)):
101+
if config.theme.custom_dir:
101102
log.error("Please remove 'custom_dir' setting.")
102103
self._help_on_customizations_and_exit()
103104

@@ -109,27 +110,57 @@ def on_config(self, config):
109110
log.error("Please remove 'hooks' setting.")
110111
self._help_on_customizations_and_exit()
111112

112-
# Assure that config_file_path is absolute.
113-
# If the --config-file option is used then the path is
114-
# used as provided, so it is likely relative.
115-
if not os.path.isabs(config.config_file_path):
116-
config.config_file_path = os.path.normpath(os.path.join(
117-
os.getcwd(),
118-
config.config_file_path
119-
))
113+
# Assure that possible relative paths, which will be validated
114+
# or used to generate other paths are absolute.
115+
config.config_file_path = _convert_to_abs(config.config_file_path)
116+
config_file_parent = os.path.dirname(config.config_file_path)
117+
118+
# The theme.custom_dir property cannot be set, therefore a helper
119+
# variable is used.
120+
custom_dir = config.theme.custom_dir
121+
if custom_dir:
122+
custom_dir = _convert_to_abs(
123+
custom_dir,
124+
abs_prefix = config_file_parent
125+
)
120126

121127
# Support projects plugin
122128
projects_plugin = config.plugins.get("material/projects")
123129
if projects_plugin:
124-
abs_projects_dir = os.path.normpath(
125-
os.path.join(
126-
os.path.dirname(config.config_file_path),
127-
projects_plugin.config.projects_dir
128-
)
130+
abs_projects_dir = _convert_to_abs(
131+
projects_plugin.config.projects_dir,
132+
abs_prefix = config_file_parent
129133
)
130134
else:
131135
abs_projects_dir = ""
132136

137+
# Load the current MkDocs config(s) to get access to INHERIT
138+
loaded_configs = _load_yaml(config.config_file_path)
139+
if not isinstance(loaded_configs, list):
140+
loaded_configs = [loaded_configs]
141+
142+
# Validate different MkDocs paths to assure that
143+
# they're children of the current working directory.
144+
paths_to_validate = [
145+
config.config_file_path,
146+
config.docs_dir,
147+
custom_dir or "",
148+
abs_projects_dir,
149+
*[cfg.get("INHERIT", "") for cfg in loaded_configs]
150+
]
151+
152+
for hook in config.hooks:
153+
path = _convert_to_abs(hook, abs_prefix = config_file_parent)
154+
paths_to_validate.append(path)
155+
156+
for path in list(paths_to_validate):
157+
if not path or path.startswith(os.getcwd()):
158+
paths_to_validate.remove(path)
159+
160+
if paths_to_validate:
161+
log.error(f"One or more paths aren't children of root")
162+
self._help_on_not_in_cwd(paths_to_validate)
163+
133164
# Create in-memory archive and prompt author for a short descriptive
134165
# name for the archive, which is also used as the directory name. Note
135166
# that the name is slugified for better readability and stripped of any
@@ -295,7 +326,28 @@ def _help_on_customizations_and_exit(self):
295326
if self.config.archive_stop_on_violation:
296327
sys.exit(1)
297328

298-
# Exclude files, which we don't want in our zip file
329+
# Print help on not in current working directory and exit
330+
def _help_on_not_in_cwd(self, bad_paths):
331+
print(Fore.RED)
332+
print(" The current working (root) directory:\n")
333+
print(f" {os.getcwd()}\n")
334+
print(" is not a parent of the following paths:")
335+
print(Style.NORMAL)
336+
for path in bad_paths:
337+
print(f" {path}")
338+
print()
339+
print(" To assure that all project files are found")
340+
print(" please adjust your config or file structure and")
341+
print(" put everything within the root directory of the project.\n")
342+
print(" Please also make sure `mkdocs build` is run in")
343+
print(" the actual root directory of the project.")
344+
print(Style.RESET_ALL)
345+
346+
# Exit, unless explicitly told not to
347+
if self.config.archive_stop_on_violation:
348+
sys.exit(1)
349+
350+
# Exclude files which we don't want in our zip file
299351
def _is_excluded(self, posix_path: str) -> bool:
300352
for pattern in self.exclusion_patterns:
301353
if regex.match(pattern, posix_path):
@@ -318,6 +370,42 @@ def _size(value, factor = 1):
318370
return f"{color}{value:3.1f} {unit}"
319371
value /= 1000.0
320372

373+
# To validate if a file is within the file tree,
374+
# it needs to be absolute, so that it is possible to
375+
# check the prefix.
376+
def _convert_to_abs(path: str, abs_prefix: str = None) -> str:
377+
if os.path.isabs(path): return path
378+
if abs_prefix is None: abs_prefix = os.getcwd()
379+
return os.path.normpath(os.path.join(abs_prefix, path))
380+
381+
# Custom YAML loader - required to handle the parent INHERIT config.
382+
# It converts the INHERIT path to absolute as a side effect.
383+
# Returns the loaded config, or a list of all loaded configs.
384+
def _load_yaml(abs_src_path: str):
385+
386+
with open(abs_src_path, "r", encoding ="utf-8-sig") as file:
387+
source = file.read()
388+
389+
try:
390+
result = yaml.load(source, Loader = get_yaml_loader()) or {}
391+
except yaml.YAMLError:
392+
result = {}
393+
394+
if "INHERIT" in result:
395+
relpath = result.get('INHERIT')
396+
parent_path = os.path.dirname(abs_src_path)
397+
abspath = _convert_to_abs(relpath, abs_prefix = parent_path)
398+
if os.path.exists(abspath):
399+
result["INHERIT"] = abspath
400+
log.debug(f"Loading inherited configuration file: {abspath}")
401+
parent = _load_yaml(abspath)
402+
if isinstance(parent, list):
403+
result = [result, *parent]
404+
elif isinstance(parent, dict):
405+
result = [result, parent]
406+
407+
return result
408+
321409
# Load info.gitignore, ignore any empty lines or # comments
322410
def _load_exclusion_patterns(path: str = None):
323411
if path is None:

Diff for: src/plugins/info/plugin.py

+104-16
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,14 @@
2727
import site
2828
import sys
2929

30+
import yaml
3031
from colorama import Fore, Style
3132
from importlib.metadata import distributions, version
3233
from io import BytesIO
3334
from markdown.extensions.toc import slugify
3435
from mkdocs.config.defaults import MkDocsConfig
3536
from mkdocs.plugins import BasePlugin, event_priority
36-
from mkdocs.utils import get_theme_dir
37+
from mkdocs.utils import get_yaml_loader
3738
import regex
3839
from zipfile import ZipFile, ZIP_DEFLATED
3940

@@ -97,7 +98,7 @@ def on_config(self, config):
9798
# hack to detect whether the custom_dir setting was used without parsing
9899
# mkdocs.yml again - we check at which position the directory provided
99100
# by the theme resides, and if it's not the first one, abort.
100-
if config.theme.dirs.index(get_theme_dir(config.theme.name)):
101+
if config.theme.custom_dir:
101102
log.error("Please remove 'custom_dir' setting.")
102103
self._help_on_customizations_and_exit()
103104

@@ -109,27 +110,57 @@ def on_config(self, config):
109110
log.error("Please remove 'hooks' setting.")
110111
self._help_on_customizations_and_exit()
111112

112-
# Assure that config_file_path is absolute.
113-
# If the --config-file option is used then the path is
114-
# used as provided, so it is likely relative.
115-
if not os.path.isabs(config.config_file_path):
116-
config.config_file_path = os.path.normpath(os.path.join(
117-
os.getcwd(),
118-
config.config_file_path
119-
))
113+
# Assure that possible relative paths, which will be validated
114+
# or used to generate other paths are absolute.
115+
config.config_file_path = _convert_to_abs(config.config_file_path)
116+
config_file_parent = os.path.dirname(config.config_file_path)
117+
118+
# The theme.custom_dir property cannot be set, therefore a helper
119+
# variable is used.
120+
custom_dir = config.theme.custom_dir
121+
if custom_dir:
122+
custom_dir = _convert_to_abs(
123+
custom_dir,
124+
abs_prefix = config_file_parent
125+
)
120126

121127
# Support projects plugin
122128
projects_plugin = config.plugins.get("material/projects")
123129
if projects_plugin:
124-
abs_projects_dir = os.path.normpath(
125-
os.path.join(
126-
os.path.dirname(config.config_file_path),
127-
projects_plugin.config.projects_dir
128-
)
130+
abs_projects_dir = _convert_to_abs(
131+
projects_plugin.config.projects_dir,
132+
abs_prefix = config_file_parent
129133
)
130134
else:
131135
abs_projects_dir = ""
132136

137+
# Load the current MkDocs config(s) to get access to INHERIT
138+
loaded_configs = _load_yaml(config.config_file_path)
139+
if not isinstance(loaded_configs, list):
140+
loaded_configs = [loaded_configs]
141+
142+
# Validate different MkDocs paths to assure that
143+
# they're children of the current working directory.
144+
paths_to_validate = [
145+
config.config_file_path,
146+
config.docs_dir,
147+
custom_dir or "",
148+
abs_projects_dir,
149+
*[cfg.get("INHERIT", "") for cfg in loaded_configs]
150+
]
151+
152+
for hook in config.hooks:
153+
path = _convert_to_abs(hook, abs_prefix = config_file_parent)
154+
paths_to_validate.append(path)
155+
156+
for path in list(paths_to_validate):
157+
if not path or path.startswith(os.getcwd()):
158+
paths_to_validate.remove(path)
159+
160+
if paths_to_validate:
161+
log.error(f"One or more paths aren't children of root")
162+
self._help_on_not_in_cwd(paths_to_validate)
163+
133164
# Create in-memory archive and prompt author for a short descriptive
134165
# name for the archive, which is also used as the directory name. Note
135166
# that the name is slugified for better readability and stripped of any
@@ -295,7 +326,28 @@ def _help_on_customizations_and_exit(self):
295326
if self.config.archive_stop_on_violation:
296327
sys.exit(1)
297328

298-
# Exclude files, which we don't want in our zip file
329+
# Print help on not in current working directory and exit
330+
def _help_on_not_in_cwd(self, bad_paths):
331+
print(Fore.RED)
332+
print(" The current working (root) directory:\n")
333+
print(f" {os.getcwd()}\n")
334+
print(" is not a parent of the following paths:")
335+
print(Style.NORMAL)
336+
for path in bad_paths:
337+
print(f" {path}")
338+
print()
339+
print(" To assure that all project files are found")
340+
print(" please adjust your config or file structure and")
341+
print(" put everything within the root directory of the project.\n")
342+
print(" Please also make sure `mkdocs build` is run in")
343+
print(" the actual root directory of the project.")
344+
print(Style.RESET_ALL)
345+
346+
# Exit, unless explicitly told not to
347+
if self.config.archive_stop_on_violation:
348+
sys.exit(1)
349+
350+
# Exclude files which we don't want in our zip file
299351
def _is_excluded(self, posix_path: str) -> bool:
300352
for pattern in self.exclusion_patterns:
301353
if regex.match(pattern, posix_path):
@@ -318,6 +370,42 @@ def _size(value, factor = 1):
318370
return f"{color}{value:3.1f} {unit}"
319371
value /= 1000.0
320372

373+
# To validate if a file is within the file tree,
374+
# it needs to be absolute, so that it is possible to
375+
# check the prefix.
376+
def _convert_to_abs(path: str, abs_prefix: str = None) -> str:
377+
if os.path.isabs(path): return path
378+
if abs_prefix is None: abs_prefix = os.getcwd()
379+
return os.path.normpath(os.path.join(abs_prefix, path))
380+
381+
# Custom YAML loader - required to handle the parent INHERIT config.
382+
# It converts the INHERIT path to absolute as a side effect.
383+
# Returns the loaded config, or a list of all loaded configs.
384+
def _load_yaml(abs_src_path: str):
385+
386+
with open(abs_src_path, "r", encoding ="utf-8-sig") as file:
387+
source = file.read()
388+
389+
try:
390+
result = yaml.load(source, Loader = get_yaml_loader()) or {}
391+
except yaml.YAMLError:
392+
result = {}
393+
394+
if "INHERIT" in result:
395+
relpath = result.get('INHERIT')
396+
parent_path = os.path.dirname(abs_src_path)
397+
abspath = _convert_to_abs(relpath, abs_prefix = parent_path)
398+
if os.path.exists(abspath):
399+
result["INHERIT"] = abspath
400+
log.debug(f"Loading inherited configuration file: {abspath}")
401+
parent = _load_yaml(abspath)
402+
if isinstance(parent, list):
403+
result = [result, *parent]
404+
elif isinstance(parent, dict):
405+
result = [result, parent]
406+
407+
return result
408+
321409
# Load info.gitignore, ignore any empty lines or # comments
322410
def _load_exclusion_patterns(path: str = None):
323411
if path is None:

0 commit comments

Comments
 (0)