import inspect from contextlib import contextmanager from itertools import chain from django.conf import settings from django.utils.decorators import method_decorator from django.utils.termcolors import colorize from sekizai.helpers import validate_template from cms import constants from cms.models import AliasPluginModel from cms.utils.compat.dj import is_installed from cms.utils.conf import get_cms_setting SUCCESS = 1 WARNING = 2 ERROR = 3 SKIPPED = 4 CHECKERS = [] class FileOutputWrapper: """ Wraps two file-like objects (that support at the very least the 'write' method) into an API to be used by the check function further down in this module. The following properties are public (and required) by alternative implementations: errors: integer count of errors encountered successes: integer count of successes encountered warnings: integer count of warnings encountered skips: integer count of skips encountered successful: Whether the checks were successful (no errors) They must also provide these methods: write_line(message=''): writes a message to stdout write_stderr_line(message=''): writes a message to stderr success(message): reports and registers a successful check error(message): reports and registers an error warn(message); reports and registers a warning skip(message): reports and registers a skipped check section(title): A context manager that starts a new section. For the Section API see FileSectionWrapper """ def __init__(self, stdout, stderr): self.stdout = stdout self.stderr = stderr self.section_wrapper = FileSectionWrapper self.errors = 0 self.successes = 0 self.warnings = 0 self.skips = 0 def colorize(self, msg, opts=(), **kwargs): return colorize(msg, opts=opts, **kwargs) def write_line(self, message=""): self.write("%s\n" % message) def write(self, message): self.stdout.write(message) def write_stderr_line(self, message=""): self.write_stderr("%s\n" % message) def write_stderr(self, message): self.stderr.write(message) def success(self, message): self.successes += 1 self.write_line("{} {}".format(message, self.colorize("[OK]", fg="green", opts=["bold"]))) def error(self, message): self.errors += 1 self.write_stderr_line("{} {}".format(message, self.colorize("[ERROR]", fg="red", opts=["bold"]))) def warn(self, message): self.warnings += 1 self.write_stderr_line("{} {}".format(message, self.colorize("[WARNING]", fg="yellow", opts=["bold"]))) def skip(self, message): self.skips += 1 self.write_line("{} {}".format(message, self.colorize("[SKIP]", fg="blue", opts=["bold"]))) @method_decorator(contextmanager) def section(self, title): self.write_line(self.colorize(title, opts=["bold"])) self.write_line(self.colorize("=" * len(title), opts=["bold"])) self.write_line() wrapper = self.section_wrapper(self) try: yield wrapper except: # NOQA self.error("Checker failed, see traceback") raise self.errors += wrapper.errors self.successes += wrapper.successes self.warnings += wrapper.warnings self.skips += wrapper.skips self.write_line("") @property def successful(self): return not self.errors class FileSectionWrapper(FileOutputWrapper): """ Used from FileOutputWrapper to report checks in a section. If you want to provide your own output class, you may want to subclass this class for the section reporting too. If you want to use your own, you must defined at least the same API as FileOutputWrapper, as well as these four additional methods: finish_success(message): End the section (successfully) finish_error(message): End the section with errors finish_warning(message): End this section with a warning finish_skip(message): End this (skipped) section """ def __init__(self, wrapper): super().__init__(wrapper.stdout, wrapper.stderr) self.wrapper = wrapper def write_line(self, message=""): self.write(" - %s\n" % message) def write_stderr_line(self, message=""): self.write_stderr(" - %s\n" % message) def finish_success(self, message): self.wrapper.write_line() self.wrapper.success(message) def finish_error(self, message): self.wrapper.write_line() self.wrapper.error(message) def finish_warning(self, message): self.wrapper.write_line() self.wrapper.warn(message) def finish_skip(self, message): self.wrapper.write_line() self.wrapper.skip(message) def define_check(func): """ Helper decorator to register a check function. """ CHECKERS.append(func) return func @define_check def check_sekizai(output): with output.section("Sekizai") as section: sekizai_installed = is_installed("sekizai") if sekizai_installed: section.success("Sekizai is installed") else: section.error("Sekizai is not installed, could not find 'sekizai' in INSTALLED_APPS") processors = list( chain(*[template["OPTIONS"].get("context_processors", []) for template in settings.TEMPLATES]) ) if "sekizai.context_processors.sekizai" in processors: section.success("Sekizai template context processor is installed") else: section.error( "Sekizai template context processor is not installed, could not find " "'sekizai.context_processors.sekizai' in TEMPLATES option context_processors" ) if not sekizai_installed: # sekizai is not installed. # we can't reliable check templates # because template loading won't work return for template, _ in get_cms_setting("TEMPLATES"): if template == constants.TEMPLATE_INHERITANCE_MAGIC: continue if validate_template(template, ["js", "css"]): section.success("Sekizai namespaces 'js' and 'css' found in %r" % template) else: section.error("Sekizai namespaces 'js' and 'css' not found in %r" % template) @define_check def check_i18n(output): with output.section("Internationalization") as section: if isinstance(getattr(settings, "CMS_LANGUAGES", {}), dict): section.success("New style CMS_LANGUAGES") else: section.warn("Old style (tuple based) CMS_LANGUAGES, please switch to the new (dictionary based) style") if getattr(settings, "LANGUAGE_CODE", "").find("_") > -1: section.warn( "LANGUAGE_CODE must contain a valid language code, not a locale (e.g.: 'en-us' instead of " "'en_US'): '%s' provided" % getattr(settings, "LANGUAGE_CODE", "") ) for lang in getattr(settings, "LANGUAGES", ()): if lang[0].find("_") > -1: section.warn( "LANGUAGES must contain valid language codes, not locales (e.g.: 'en-us' instead of " "'en_US'): '%s' provided" % lang[0] ) site_id = getattr(settings, "SITE_ID", None) if site_id == hash(site_id): for site, items in get_cms_setting("LANGUAGES").items(): if isinstance(site, int): for lang in items: if lang["code"].find("_") > -1: section.warn( "CMS_LANGUAGES entries must contain valid language codes, not locales (e.g.: " "'en-us' instead of 'en_US'): '%s' provided" % lang["code"] ) @define_check def check_site_id(output): with output.section("Site configuration") as section: if not is_installed("django.contrib.sites"): section.warn("django.contrib.sites is not in INSTALLED_APPS") else: section.success("django.contrib.sites is in INSTALLED_APPS") site_id = getattr(settings, "SITE_ID", None) if site_id is None: section.warn("SITE_ID is not set - site identifictation will fall back to request host header or custom middleware") elif site_id != hash(site_id): section.error("SITE_ID must be an integer, not %r" % site_id) else: section.success("SITE_ID is set to %r" % site_id) @define_check def check_middlewares(output): with output.section("Middlewares") as section: required_middlewares = ( "django.contrib.sessions.middleware.SessionMiddleware", "django.middleware.csrf.CsrfViewMiddleware", "django.contrib.auth.middleware.AuthenticationMiddleware", "django.contrib.messages.middleware.MessageMiddleware", "django.middleware.locale.LocaleMiddleware", "django.middleware.common.CommonMiddleware", "cms.middleware.user.CurrentUserMiddleware", "cms.middleware.page.CurrentPageMiddleware", "cms.middleware.toolbar.ToolbarMiddleware", "cms.middleware.language.LanguageCookieMiddleware", ) middlewares = getattr(settings, "MIDDLEWARE", []) for middleware in required_middlewares: if middleware not in middlewares: section.error("%s middleware must be in MIDDLEWARE" % middleware) else: section.success("%s middleware found" % middleware) @define_check def check_context_processors(output): with output.section("Context processors") as section: processors = list( chain(*[template["OPTIONS"].get("context_processors", []) for template in settings.TEMPLATES]) ) required_processors = ("cms.context_processors.cms_settings", "django.template.context_processors.i18n") for processor in required_processors: if processor not in processors: section.error("%s context processor must be in TEMPLATES option context_processors" % processor) else: section.success("%s context processor found" % processor) @define_check def check_plugin_instances(output): from cms.management.commands.subcommands.list import plugin_report with output.section("Plugin instances") as section: # get the report report = plugin_report() section.success("Plugin instances of %s types found in the database" % len(report)) # loop over plugin types in the report for plugin_type in report: # warn about those that are not installed if not plugin_type["model"]: section.error("%s has instances but is no longer installed" % plugin_type["type"]) # warn about those that have unsaved instances if plugin_type["unsaved_instances"]: section.error( "{} has {} unsaved instances".format(plugin_type["type"], len(plugin_type["unsaved_instances"])) ) if not section.successful: section.finish_error( "There are potentially serious problems with the plugins in your database. \nEven if " "your site works, you should run the 'manage.py cms list plugins' \ncommand and then " "the 'manage.py cms delete-orphaned-plugins' command. \nThis will alter your " "database; read the documentation before using it." ) @define_check def check_copy_relations(output): from cms.extensions import extension_pool from cms.extensions.models import BaseExtension from cms.models.pluginmodel import CMSPlugin from cms.plugin_pool import plugin_pool def c_to_s(klass): return f"{klass.__module__}.{klass.__name__}" def get_class(method_name, model): for cls in inspect.getmro(model): if method_name in cls.__dict__: return cls return None with output.section('Presence of "copy_relations"') as section: plugin_pool.discover_plugins() for plugin in plugin_pool.plugins.values(): plugin_class = plugin.model if get_class("copy_relations", plugin_class) is not CMSPlugin or plugin_class is CMSPlugin: # this class defines a ``copy_relations`` method, nothing more # to do continue for rel in plugin_class._meta.many_to_many: section.warn( f'{c_to_s(plugin_class)} has a many-to-many relation to {c_to_s(rel.model)},\n but no "copy_relations" method defined.' ) for rel in plugin_class._get_related_objects(): if ( rel.model != CMSPlugin and not issubclass(rel.model, plugin.model) and rel.model != AliasPluginModel ): section.warn( f'{c_to_s(plugin_class)} has a foreign key from {c_to_s(rel.model)},\n but no "copy_relations" method defined.' ) for extension in chain(extension_pool.page_extensions, extension_pool.page_content_extensions): if get_class("copy_relations", extension) is not BaseExtension: # OK, looks like there is a 'copy_relations' defined in the # extension... move along... continue for rel in extension._meta.many_to_many: section.warn( "%s has a many-to-many relation to %s,\n " 'but no "copy_relations" method defined.' % ( c_to_s(extension), c_to_s(rel.remote_field.model), ) ) for rel in extension._get_related_objects(): if rel.model != extension: section.warn( f'{c_to_s(extension)} has a foreign key from {c_to_s(rel.model)},\n but no "copy_relations" method defined.' ) if not section.warnings: section.finish_success( 'All plugins and page/page content extensions have "copy_relations" method if needed.' ) else: section.finish_success( 'Some plugins or page/page content extensions do not define a "copy_relations" method.\n' "This might lead to data loss when publishing or copying plugins/extensions.\n" "See https://docs.django-cms.org/en/latest/how_to/09-custom_plugins.html#handling-relations or " # noqa "https://docs.django-cms.org/en/latest/how_to/18-extending_page_contents.html#handling-relations." ) @define_check def check_template_conf(output): with output.section("Template configuration") as section: if get_cms_setting("TEMPLATES"): if isinstance(get_cms_setting("TEMPLATES"), (list, tuple)): for template in get_cms_setting("TEMPLATES"): if not isinstance(template, (list, tuple)): section.error("CMS_TEMPLATES setting contains a non-list/tuple entry") elif len(template) != 2: section.error("CMS_TEMPLATES setting contains a list/tuple with != 2 entries") elif not isinstance(template[0], str): section.error("CMS_TEMPLATES contains a non-string entry") else: section.success("CMS_TEMPLATES_DIR or CMS_TEMPLATES setting found") else: section.error("CMS_TEMPLATES setting is not a list or tuple") if hasattr(settings, "CMS_PLACEHOLDERS"): section.warn("CMS_PLACEHOLDERS setting is also present but will be ignored.") elif get_cms_setting("PLACEHOLDERS"): if isinstance(get_cms_setting("PLACEHOLDERS"), (list, tuple)): for placeholder in get_cms_setting("PLACEHOLDERS"): if not isinstance(placeholder, (list, tuple)): section.error("CMS_PLACEHOLDERS setting contains a non-list/tuple entry") elif not isinstance(placeholder[0], str): section.error( f"CMS_PLACEHOLDERS contains an entry with a non-string identifier: {placeholder[0]}" ) else: section.success("CMS_PLACEHOLDERS setting entry found - CMS will run in headless mode") else: section.error("CMS_PLACEHOLDERS setting is not a list or tuple") else: section.warn( "Both CMS_TEMPLATES and CMS_PLACEHOLDERS settings are missing. " 'Will run in headless mode with one placeholder called "content"' ) @define_check def check_cmsapps_names(output): from cms.apphook_pool import apphook_pool with output.section("Apphooks") as section: apphooks = apphook_pool.get_apphooks() for hook, name in apphooks: if apphook_pool.get_apphook(hook).name is None: section.warn("CMSApps should define a name. %s doesn't have a name" % name) else: section.success("CMSApp %s has a name defined" % name) if not apphooks: section.finish_success("CMSApps configuration is okay") @define_check def check_plugin_parent_child_relations(output): from cms.plugin_pool import plugin_pool with output.section("Plugin parent/child relations") as section: plugin_pool.discover_plugins() for plugin in plugin_pool.plugins.values(): # Check parent_classes if hasattr(plugin, "parent_classes") and plugin.parent_classes: for parent_class in plugin.parent_classes: if parent_class not in ("0", "") and parent_class not in plugin_pool.plugins: section.warn(f"Parent plugin class '{parent_class}' not found for plugin '{plugin.__name__}'") # Check child_classes if hasattr(plugin, "child_classes") and plugin.child_classes: for child_class in plugin.child_classes: if child_class not in plugin_pool.plugins: section.warn(f"Child plugin class '{child_class}' not found for plugin '{plugin.__name__}'") if section.successful: section.finish_success("Plugin parent/child relations are valid") else: section.finish_warning( "Some plugins have invalid parent/child class configurations.\n" "This might cause unexpected behavior in the admin interface." ) def check(output): """ Checks the configuration/environment of this django CMS installation. 'output' should be an object that provides the same API as FileOutputWrapper. Returns whether the configuration/environment are okay (has no errors) """ import cms title = f"Checking django CMS {cms.__version__} installation" border = "*" * len(title) output.write_line(output.colorize(border, opts=["bold"])) output.write_line(output.colorize(title, opts=["bold"])) output.write_line(output.colorize(border, opts=["bold"])) output.write_line() for checker in CHECKERS: checker(output) output.write_line() with output.section("OVERALL RESULTS"): if output.errors: output.write_stderr_line(output.colorize("%s errors!" % output.errors, opts=["bold"], fg="red")) if output.warnings: output.write_stderr_line(output.colorize("%s warnings!" % output.warnings, opts=["bold"], fg="yellow")) if output.skips: output.write_line(output.colorize("%s checks skipped!" % output.skips, opts=["bold"], fg="blue")) output.write_line(output.colorize("%s checks successful!" % output.successes, opts=["bold"], fg="green")) output.write_line() if output.errors: output.write_stderr_line(output.colorize("Please check the errors above", opts=["bold"], fg="red")) elif output.warnings: output.write_stderr_line( output.colorize("Installation okay, but please check warnings above", opts=["bold"], fg="yellow") ) else: output.write_line(output.colorize("Installation okay", opts=["bold"], fg="green")) return output.successful