plain 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (169) hide show
  1. plain/README.md +33 -0
  2. plain/__main__.py +5 -0
  3. plain/assets/README.md +56 -0
  4. plain/assets/__init__.py +6 -0
  5. plain/assets/finders.py +233 -0
  6. plain/assets/preflight.py +14 -0
  7. plain/assets/storage.py +916 -0
  8. plain/assets/utils.py +52 -0
  9. plain/assets/whitenoise/__init__.py +5 -0
  10. plain/assets/whitenoise/base.py +259 -0
  11. plain/assets/whitenoise/compress.py +189 -0
  12. plain/assets/whitenoise/media_types.py +137 -0
  13. plain/assets/whitenoise/middleware.py +197 -0
  14. plain/assets/whitenoise/responders.py +286 -0
  15. plain/assets/whitenoise/storage.py +178 -0
  16. plain/assets/whitenoise/string_utils.py +13 -0
  17. plain/cli/README.md +123 -0
  18. plain/cli/__init__.py +3 -0
  19. plain/cli/cli.py +439 -0
  20. plain/cli/formatting.py +61 -0
  21. plain/cli/packages.py +73 -0
  22. plain/cli/print.py +9 -0
  23. plain/cli/startup.py +33 -0
  24. plain/csrf/README.md +3 -0
  25. plain/csrf/middleware.py +466 -0
  26. plain/csrf/views.py +10 -0
  27. plain/debug.py +23 -0
  28. plain/exceptions.py +242 -0
  29. plain/forms/README.md +14 -0
  30. plain/forms/__init__.py +8 -0
  31. plain/forms/boundfield.py +58 -0
  32. plain/forms/exceptions.py +11 -0
  33. plain/forms/fields.py +1030 -0
  34. plain/forms/forms.py +297 -0
  35. plain/http/README.md +1 -0
  36. plain/http/__init__.py +51 -0
  37. plain/http/cookie.py +20 -0
  38. plain/http/multipartparser.py +743 -0
  39. plain/http/request.py +754 -0
  40. plain/http/response.py +719 -0
  41. plain/internal/__init__.py +0 -0
  42. plain/internal/files/README.md +3 -0
  43. plain/internal/files/__init__.py +3 -0
  44. plain/internal/files/base.py +161 -0
  45. plain/internal/files/locks.py +127 -0
  46. plain/internal/files/move.py +102 -0
  47. plain/internal/files/temp.py +79 -0
  48. plain/internal/files/uploadedfile.py +150 -0
  49. plain/internal/files/uploadhandler.py +254 -0
  50. plain/internal/files/utils.py +78 -0
  51. plain/internal/handlers/__init__.py +0 -0
  52. plain/internal/handlers/base.py +133 -0
  53. plain/internal/handlers/exception.py +145 -0
  54. plain/internal/handlers/wsgi.py +216 -0
  55. plain/internal/legacy/__init__.py +0 -0
  56. plain/internal/legacy/__main__.py +12 -0
  57. plain/internal/legacy/management/__init__.py +414 -0
  58. plain/internal/legacy/management/base.py +692 -0
  59. plain/internal/legacy/management/color.py +113 -0
  60. plain/internal/legacy/management/commands/__init__.py +0 -0
  61. plain/internal/legacy/management/commands/collectstatic.py +297 -0
  62. plain/internal/legacy/management/sql.py +67 -0
  63. plain/internal/legacy/management/utils.py +175 -0
  64. plain/json.py +40 -0
  65. plain/logs/README.md +24 -0
  66. plain/logs/__init__.py +5 -0
  67. plain/logs/configure.py +39 -0
  68. plain/logs/loggers.py +74 -0
  69. plain/logs/utils.py +46 -0
  70. plain/middleware/README.md +3 -0
  71. plain/middleware/__init__.py +0 -0
  72. plain/middleware/clickjacking.py +52 -0
  73. plain/middleware/common.py +87 -0
  74. plain/middleware/gzip.py +64 -0
  75. plain/middleware/security.py +64 -0
  76. plain/packages/README.md +41 -0
  77. plain/packages/__init__.py +4 -0
  78. plain/packages/config.py +259 -0
  79. plain/packages/registry.py +438 -0
  80. plain/paginator.py +187 -0
  81. plain/preflight/README.md +3 -0
  82. plain/preflight/__init__.py +38 -0
  83. plain/preflight/compatibility/__init__.py +0 -0
  84. plain/preflight/compatibility/django_4_0.py +20 -0
  85. plain/preflight/files.py +19 -0
  86. plain/preflight/messages.py +88 -0
  87. plain/preflight/registry.py +72 -0
  88. plain/preflight/security/__init__.py +0 -0
  89. plain/preflight/security/base.py +268 -0
  90. plain/preflight/security/csrf.py +40 -0
  91. plain/preflight/urls.py +117 -0
  92. plain/runtime/README.md +75 -0
  93. plain/runtime/__init__.py +61 -0
  94. plain/runtime/global_settings.py +199 -0
  95. plain/runtime/user_settings.py +353 -0
  96. plain/signals/README.md +14 -0
  97. plain/signals/__init__.py +5 -0
  98. plain/signals/dispatch/__init__.py +9 -0
  99. plain/signals/dispatch/dispatcher.py +320 -0
  100. plain/signals/dispatch/license.txt +35 -0
  101. plain/signing.py +299 -0
  102. plain/templates/README.md +20 -0
  103. plain/templates/__init__.py +6 -0
  104. plain/templates/core.py +24 -0
  105. plain/templates/jinja/README.md +227 -0
  106. plain/templates/jinja/__init__.py +22 -0
  107. plain/templates/jinja/defaults.py +119 -0
  108. plain/templates/jinja/extensions.py +39 -0
  109. plain/templates/jinja/filters.py +28 -0
  110. plain/templates/jinja/globals.py +19 -0
  111. plain/test/README.md +3 -0
  112. plain/test/__init__.py +16 -0
  113. plain/test/client.py +985 -0
  114. plain/test/utils.py +255 -0
  115. plain/urls/README.md +3 -0
  116. plain/urls/__init__.py +40 -0
  117. plain/urls/base.py +118 -0
  118. plain/urls/conf.py +94 -0
  119. plain/urls/converters.py +66 -0
  120. plain/urls/exceptions.py +9 -0
  121. plain/urls/resolvers.py +731 -0
  122. plain/utils/README.md +3 -0
  123. plain/utils/__init__.py +0 -0
  124. plain/utils/_os.py +52 -0
  125. plain/utils/cache.py +327 -0
  126. plain/utils/connection.py +84 -0
  127. plain/utils/crypto.py +76 -0
  128. plain/utils/datastructures.py +345 -0
  129. plain/utils/dateformat.py +329 -0
  130. plain/utils/dateparse.py +154 -0
  131. plain/utils/dates.py +76 -0
  132. plain/utils/deconstruct.py +54 -0
  133. plain/utils/decorators.py +90 -0
  134. plain/utils/deprecation.py +6 -0
  135. plain/utils/duration.py +44 -0
  136. plain/utils/email.py +12 -0
  137. plain/utils/encoding.py +235 -0
  138. plain/utils/functional.py +456 -0
  139. plain/utils/hashable.py +26 -0
  140. plain/utils/html.py +401 -0
  141. plain/utils/http.py +374 -0
  142. plain/utils/inspect.py +73 -0
  143. plain/utils/ipv6.py +46 -0
  144. plain/utils/itercompat.py +8 -0
  145. plain/utils/module_loading.py +69 -0
  146. plain/utils/regex_helper.py +353 -0
  147. plain/utils/safestring.py +72 -0
  148. plain/utils/termcolors.py +221 -0
  149. plain/utils/text.py +518 -0
  150. plain/utils/timesince.py +138 -0
  151. plain/utils/timezone.py +244 -0
  152. plain/utils/tree.py +126 -0
  153. plain/validators.py +603 -0
  154. plain/views/README.md +268 -0
  155. plain/views/__init__.py +18 -0
  156. plain/views/base.py +107 -0
  157. plain/views/csrf.py +24 -0
  158. plain/views/errors.py +25 -0
  159. plain/views/exceptions.py +4 -0
  160. plain/views/forms.py +76 -0
  161. plain/views/objects.py +229 -0
  162. plain/views/redirect.py +72 -0
  163. plain/views/templates.py +66 -0
  164. plain/wsgi.py +11 -0
  165. plain-0.1.0.dist-info/LICENSE +85 -0
  166. plain-0.1.0.dist-info/METADATA +51 -0
  167. plain-0.1.0.dist-info/RECORD +169 -0
  168. plain-0.1.0.dist-info/WHEEL +4 -0
  169. plain-0.1.0.dist-info/entry_points.txt +3 -0
plain/paginator.py ADDED
@@ -0,0 +1,187 @@
1
+ import collections.abc
2
+ import inspect
3
+ import warnings
4
+ from math import ceil
5
+
6
+ from plain.utils.functional import cached_property
7
+ from plain.utils.inspect import method_has_no_args
8
+
9
+
10
+ class UnorderedObjectListWarning(RuntimeWarning):
11
+ pass
12
+
13
+
14
+ class InvalidPage(Exception):
15
+ pass
16
+
17
+
18
+ class PageNotAnInteger(InvalidPage):
19
+ pass
20
+
21
+
22
+ class EmptyPage(InvalidPage):
23
+ pass
24
+
25
+
26
+ class Paginator:
27
+ # Translators: String used to replace omitted page numbers in elided page
28
+ # range generated by paginators, e.g. [1, 2, '…', 5, 6, 7, '…', 9, 10].
29
+ ELLIPSIS = "…"
30
+
31
+ def __init__(self, object_list, per_page, orphans=0, allow_empty_first_page=True):
32
+ self.object_list = object_list
33
+ self._check_object_list_is_ordered()
34
+ self.per_page = int(per_page)
35
+ self.orphans = int(orphans)
36
+ self.allow_empty_first_page = allow_empty_first_page
37
+
38
+ def __iter__(self):
39
+ for page_number in self.page_range:
40
+ yield self.page(page_number)
41
+
42
+ def validate_number(self, number):
43
+ """Validate the given 1-based page number."""
44
+ try:
45
+ if isinstance(number, float) and not number.is_integer():
46
+ raise ValueError
47
+ number = int(number)
48
+ except (TypeError, ValueError):
49
+ raise PageNotAnInteger("That page number is not an integer")
50
+ if number < 1:
51
+ raise EmptyPage("That page number is less than 1")
52
+ if number > self.num_pages:
53
+ raise EmptyPage("That page contains no results")
54
+ return number
55
+
56
+ def get_page(self, number):
57
+ """
58
+ Return a valid page, even if the page argument isn't a number or isn't
59
+ in range.
60
+ """
61
+ try:
62
+ number = self.validate_number(number)
63
+ except PageNotAnInteger:
64
+ number = 1
65
+ except EmptyPage:
66
+ number = self.num_pages
67
+ return self.page(number)
68
+
69
+ def page(self, number):
70
+ """Return a Page object for the given 1-based page number."""
71
+ number = self.validate_number(number)
72
+ bottom = (number - 1) * self.per_page
73
+ top = bottom + self.per_page
74
+ if top + self.orphans >= self.count:
75
+ top = self.count
76
+ return self._get_page(self.object_list[bottom:top], number, self)
77
+
78
+ def _get_page(self, *args, **kwargs):
79
+ """
80
+ Return an instance of a single page.
81
+
82
+ This hook can be used by subclasses to use an alternative to the
83
+ standard :cls:`Page` object.
84
+ """
85
+ return Page(*args, **kwargs)
86
+
87
+ @cached_property
88
+ def count(self):
89
+ """Return the total number of objects, across all pages."""
90
+ c = getattr(self.object_list, "count", None)
91
+ if callable(c) and not inspect.isbuiltin(c) and method_has_no_args(c):
92
+ return c()
93
+ return len(self.object_list)
94
+
95
+ @cached_property
96
+ def num_pages(self):
97
+ """Return the total number of pages."""
98
+ if self.count == 0 and not self.allow_empty_first_page:
99
+ return 0
100
+ hits = max(1, self.count - self.orphans)
101
+ return ceil(hits / self.per_page)
102
+
103
+ @property
104
+ def page_range(self):
105
+ """
106
+ Return a 1-based range of pages for iterating through within
107
+ a template for loop.
108
+ """
109
+ return range(1, self.num_pages + 1)
110
+
111
+ def _check_object_list_is_ordered(self):
112
+ """
113
+ Warn if self.object_list is unordered (typically a QuerySet).
114
+ """
115
+ ordered = getattr(self.object_list, "ordered", None)
116
+ if ordered is not None and not ordered:
117
+ obj_list_repr = (
118
+ f"{self.object_list.model} {self.object_list.__class__.__name__}"
119
+ if hasattr(self.object_list, "model")
120
+ else f"{self.object_list!r}"
121
+ )
122
+ warnings.warn(
123
+ "Pagination may yield inconsistent results with an unordered "
124
+ f"object_list: {obj_list_repr}.",
125
+ UnorderedObjectListWarning,
126
+ stacklevel=3,
127
+ )
128
+
129
+
130
+ class Page(collections.abc.Sequence):
131
+ def __init__(self, object_list, number, paginator):
132
+ self.object_list = object_list
133
+ self.number = number
134
+ self.paginator = paginator
135
+
136
+ def __repr__(self):
137
+ return f"<Page {self.number} of {self.paginator.num_pages}>"
138
+
139
+ def __len__(self):
140
+ return len(self.object_list)
141
+
142
+ def __getitem__(self, index):
143
+ if not isinstance(index, int | slice):
144
+ raise TypeError(
145
+ "Page indices must be integers or slices, not %s."
146
+ % type(index).__name__
147
+ )
148
+ # The object_list is converted to a list so that if it was a QuerySet
149
+ # it won't be a database hit per __getitem__.
150
+ if not isinstance(self.object_list, list):
151
+ self.object_list = list(self.object_list)
152
+ return self.object_list[index]
153
+
154
+ def has_next(self):
155
+ return self.number < self.paginator.num_pages
156
+
157
+ def has_previous(self):
158
+ return self.number > 1
159
+
160
+ def has_other_pages(self):
161
+ return self.has_previous() or self.has_next()
162
+
163
+ def next_page_number(self):
164
+ return self.paginator.validate_number(self.number + 1)
165
+
166
+ def previous_page_number(self):
167
+ return self.paginator.validate_number(self.number - 1)
168
+
169
+ def start_index(self):
170
+ """
171
+ Return the 1-based index of the first object on this page,
172
+ relative to total objects in the paginator.
173
+ """
174
+ # Special case, return zero if no items.
175
+ if self.paginator.count == 0:
176
+ return 0
177
+ return (self.paginator.per_page * (self.number - 1)) + 1
178
+
179
+ def end_index(self):
180
+ """
181
+ Return the 1-based index of the last object on this page,
182
+ relative to total objects found (hits).
183
+ """
184
+ # Special case for the last page because there can be orphans.
185
+ if self.number == self.paginator.num_pages:
186
+ return self.paginator.count
187
+ return self.number * self.paginator.per_page
@@ -0,0 +1,3 @@
1
+ # Checks
2
+
3
+ Static system checks to catch misconfigurations.
@@ -0,0 +1,38 @@
1
+ from .messages import (
2
+ CRITICAL,
3
+ DEBUG,
4
+ ERROR,
5
+ INFO,
6
+ WARNING,
7
+ CheckMessage,
8
+ Critical,
9
+ Debug,
10
+ Error,
11
+ Info,
12
+ Warning,
13
+ )
14
+ from .registry import register, run_checks
15
+
16
+ # Import these to force registration of checks
17
+ import plain.preflight.compatibility.django_4_0 # NOQA isort:skip
18
+ import plain.preflight.files # NOQA isort:skip
19
+ import plain.preflight.security.base # NOQA isort:skip
20
+ import plain.preflight.security.csrf # NOQA isort:skip
21
+ import plain.preflight.urls # NOQA isort:skip
22
+
23
+
24
+ __all__ = [
25
+ "CheckMessage",
26
+ "Debug",
27
+ "Info",
28
+ "Warning",
29
+ "Error",
30
+ "Critical",
31
+ "DEBUG",
32
+ "INFO",
33
+ "WARNING",
34
+ "ERROR",
35
+ "CRITICAL",
36
+ "register",
37
+ "run_checks",
38
+ ]
File without changes
@@ -0,0 +1,20 @@
1
+ from plain.runtime import settings
2
+
3
+ from .. import Error, register
4
+
5
+
6
+ @register
7
+ def check_csrf_trusted_origins(package_configs, **kwargs):
8
+ errors = []
9
+ for origin in settings.CSRF_TRUSTED_ORIGINS:
10
+ if "://" not in origin:
11
+ errors.append(
12
+ Error(
13
+ "As of Plain 4.0, the values in the CSRF_TRUSTED_ORIGINS "
14
+ "setting must start with a scheme (usually http:// or "
15
+ "https://) but found %s. See the release notes for details."
16
+ % origin,
17
+ id="4_0.E001",
18
+ )
19
+ )
20
+ return errors
@@ -0,0 +1,19 @@
1
+ from pathlib import Path
2
+
3
+ from plain.runtime import settings
4
+
5
+ from . import Error, register
6
+
7
+
8
+ @register
9
+ def check_setting_file_upload_temp_dir(package_configs, **kwargs):
10
+ setting = getattr(settings, "FILE_UPLOAD_TEMP_DIR", None)
11
+ if setting and not Path(setting).is_dir():
12
+ return [
13
+ Error(
14
+ f"The FILE_UPLOAD_TEMP_DIR setting refers to the nonexistent "
15
+ f"directory '{setting}'.",
16
+ id="files.E001",
17
+ ),
18
+ ]
19
+ return []
@@ -0,0 +1,88 @@
1
+ # Levels
2
+ DEBUG = 10
3
+ INFO = 20
4
+ WARNING = 30
5
+ ERROR = 40
6
+ CRITICAL = 50
7
+
8
+
9
+ class CheckMessage:
10
+ def __init__(self, level, msg, hint=None, obj=None, id=None):
11
+ if not isinstance(level, int):
12
+ raise TypeError("The first argument should be level.")
13
+ self.level = level
14
+ self.msg = msg
15
+ self.hint = hint
16
+ self.obj = obj
17
+ self.id = id
18
+
19
+ def __eq__(self, other):
20
+ return isinstance(other, self.__class__) and all(
21
+ getattr(self, attr) == getattr(other, attr)
22
+ for attr in ["level", "msg", "hint", "obj", "id"]
23
+ )
24
+
25
+ def __str__(self):
26
+ try:
27
+ from plain import models
28
+
29
+ ModelBase = models.base.ModelBase
30
+ using_db = True
31
+ except ImportError:
32
+ using_db = False
33
+ ModelBase = object
34
+
35
+ if self.obj is None:
36
+ obj = "?"
37
+ elif using_db and isinstance(self.obj, ModelBase):
38
+ # We need to hardcode ModelBase and Field cases because its __str__
39
+ # method doesn't return "applabel.modellabel" and cannot be changed.
40
+ obj = self.obj._meta.label
41
+ else:
42
+ obj = str(self.obj)
43
+ id = "(%s) " % self.id if self.id else ""
44
+ hint = "\n\tHINT: %s" % self.hint if self.hint else ""
45
+ return f"{obj}: {id}{self.msg}{hint}"
46
+
47
+ def __repr__(self):
48
+ return "<{}: level={!r}, msg={!r}, hint={!r}, obj={!r}, id={!r}>".format(
49
+ self.__class__.__name__,
50
+ self.level,
51
+ self.msg,
52
+ self.hint,
53
+ self.obj,
54
+ self.id,
55
+ )
56
+
57
+ def is_serious(self, level=ERROR):
58
+ return self.level >= level
59
+
60
+ def is_silenced(self):
61
+ from plain.runtime import settings
62
+
63
+ return self.id in settings.SILENCED_PREFLIGHT_CHECKS
64
+
65
+
66
+ class Debug(CheckMessage):
67
+ def __init__(self, *args, **kwargs):
68
+ super().__init__(DEBUG, *args, **kwargs)
69
+
70
+
71
+ class Info(CheckMessage):
72
+ def __init__(self, *args, **kwargs):
73
+ super().__init__(INFO, *args, **kwargs)
74
+
75
+
76
+ class Warning(CheckMessage):
77
+ def __init__(self, *args, **kwargs):
78
+ super().__init__(WARNING, *args, **kwargs)
79
+
80
+
81
+ class Error(CheckMessage):
82
+ def __init__(self, *args, **kwargs):
83
+ super().__init__(ERROR, *args, **kwargs)
84
+
85
+
86
+ class Critical(CheckMessage):
87
+ def __init__(self, *args, **kwargs):
88
+ super().__init__(CRITICAL, *args, **kwargs)
@@ -0,0 +1,72 @@
1
+ from plain.utils.inspect import func_accepts_kwargs
2
+ from plain.utils.itercompat import is_iterable
3
+
4
+
5
+ class CheckRegistry:
6
+ def __init__(self):
7
+ self.registered_checks = set()
8
+ self.deployment_checks = set()
9
+
10
+ def register(self, check=None, deploy=False):
11
+ """
12
+ Can be used as a function or a decorator. Register given function
13
+ `f`. The function should receive **kwargs
14
+ and return list of Errors and Warnings.
15
+
16
+ Example::
17
+
18
+ registry = CheckRegistry()
19
+ @registry.register('mytag', 'anothertag')
20
+ def my_check(package_configs, **kwargs):
21
+ # ... perform checks and collect `errors` ...
22
+ return errors
23
+ # or
24
+ registry.register(my_check, 'mytag', 'anothertag')
25
+ """
26
+
27
+ def inner(check):
28
+ if not func_accepts_kwargs(check):
29
+ raise TypeError(
30
+ "Check functions must accept keyword arguments (**kwargs)."
31
+ )
32
+ checks = self.deployment_checks if deploy else self.registered_checks
33
+ checks.add(check)
34
+ return check
35
+
36
+ if callable(check):
37
+ return inner(check)
38
+ else:
39
+ return inner
40
+
41
+ def run_checks(
42
+ self,
43
+ package_configs=None,
44
+ include_deployment_checks=False,
45
+ databases=None,
46
+ ):
47
+ """
48
+ Run all registered checks and return list of Errors and Warnings.
49
+ """
50
+ errors = []
51
+ checks = self.get_checks(include_deployment_checks)
52
+
53
+ for check in checks:
54
+ new_errors = check(package_configs=package_configs, databases=databases)
55
+ if not is_iterable(new_errors):
56
+ raise TypeError(
57
+ "The function %r did not return a list. All functions "
58
+ "registered with the checks registry must return a list." % check,
59
+ )
60
+ errors.extend(new_errors)
61
+ return errors
62
+
63
+ def get_checks(self, include_deployment_checks=False):
64
+ checks = list(self.registered_checks)
65
+ if include_deployment_checks:
66
+ checks.extend(self.deployment_checks)
67
+ return checks
68
+
69
+
70
+ registry = CheckRegistry()
71
+ register = registry.register
72
+ run_checks = registry.run_checks
File without changes
@@ -0,0 +1,268 @@
1
+ from plain.exceptions import ImproperlyConfigured
2
+ from plain.runtime import settings
3
+
4
+ from .. import Error, Warning, register
5
+
6
+ CROSS_ORIGIN_OPENER_POLICY_VALUES = {
7
+ "same-origin",
8
+ "same-origin-allow-popups",
9
+ "unsafe-none",
10
+ }
11
+ REFERRER_POLICY_VALUES = {
12
+ "no-referrer",
13
+ "no-referrer-when-downgrade",
14
+ "origin",
15
+ "origin-when-cross-origin",
16
+ "same-origin",
17
+ "strict-origin",
18
+ "strict-origin-when-cross-origin",
19
+ "unsafe-url",
20
+ }
21
+
22
+ SECRET_KEY_INSECURE_PREFIX = "plain-insecure-"
23
+ SECRET_KEY_MIN_LENGTH = 50
24
+ SECRET_KEY_MIN_UNIQUE_CHARACTERS = 5
25
+
26
+ SECRET_KEY_WARNING_MSG = (
27
+ f"Your %s has less than {SECRET_KEY_MIN_LENGTH} characters, less than "
28
+ f"{SECRET_KEY_MIN_UNIQUE_CHARACTERS} unique characters, or it's prefixed "
29
+ f"with '{SECRET_KEY_INSECURE_PREFIX}' indicating that it was generated "
30
+ f"automatically by Plain. Please generate a long and random value, "
31
+ f"otherwise many of Plain's security-critical features will be "
32
+ f"vulnerable to attack."
33
+ )
34
+
35
+ W001 = Warning(
36
+ "You do not have 'plain.middleware.security.SecurityMiddleware' "
37
+ "in your MIDDLEWARE so the SECURE_HSTS_SECONDS, "
38
+ "SECURE_CONTENT_TYPE_NOSNIFF, SECURE_REFERRER_POLICY, "
39
+ "SECURE_CROSS_ORIGIN_OPENER_POLICY, and SECURE_SSL_REDIRECT settings will "
40
+ "have no effect.",
41
+ id="security.W001",
42
+ )
43
+
44
+ W002 = Warning(
45
+ "You do not have "
46
+ "'plain.middleware.clickjacking.XFrameOptionsMiddleware' in your "
47
+ "MIDDLEWARE, so your pages will not be served with an "
48
+ "'x-frame-options' header. Unless there is a good reason for your "
49
+ "site to be served in a frame, you should consider enabling this "
50
+ "header to help prevent clickjacking attacks.",
51
+ id="security.W002",
52
+ )
53
+
54
+ W004 = Warning(
55
+ "You have not set a value for the SECURE_HSTS_SECONDS setting. "
56
+ "If your entire site is served only over SSL, you may want to consider "
57
+ "setting a value and enabling HTTP Strict Transport Security. "
58
+ "Be sure to read the documentation first; enabling HSTS carelessly "
59
+ "can cause serious, irreversible problems.",
60
+ id="security.W004",
61
+ )
62
+
63
+ W005 = Warning(
64
+ "You have not set the SECURE_HSTS_INCLUDE_SUBDOMAINS setting to True. "
65
+ "Without this, your site is potentially vulnerable to attack "
66
+ "via an insecure connection to a subdomain. Only set this to True if "
67
+ "you are certain that all subdomains of your domain should be served "
68
+ "exclusively via SSL.",
69
+ id="security.W005",
70
+ )
71
+
72
+ W006 = Warning(
73
+ "Your SECURE_CONTENT_TYPE_NOSNIFF setting is not set to True, "
74
+ "so your pages will not be served with an "
75
+ "'X-Content-Type-Options: nosniff' header. "
76
+ "You should consider enabling this header to prevent the "
77
+ "browser from identifying content types incorrectly.",
78
+ id="security.W006",
79
+ )
80
+
81
+ W008 = Warning(
82
+ "Your SECURE_SSL_REDIRECT setting is not set to True. "
83
+ "Unless your site should be available over both SSL and non-SSL "
84
+ "connections, you may want to either set this setting True "
85
+ "or configure a load balancer or reverse-proxy server "
86
+ "to redirect all connections to HTTPS.",
87
+ id="security.W008",
88
+ )
89
+
90
+ W009 = Warning(
91
+ SECRET_KEY_WARNING_MSG % "SECRET_KEY",
92
+ id="security.W009",
93
+ )
94
+
95
+ W018 = Warning(
96
+ "You should not have DEBUG set to True in deployment.",
97
+ id="security.W018",
98
+ )
99
+
100
+ W020 = Warning(
101
+ "ALLOWED_HOSTS must not be empty in deployment.",
102
+ id="security.W020",
103
+ )
104
+
105
+ W021 = Warning(
106
+ "You have not set the SECURE_HSTS_PRELOAD setting to True. Without this, "
107
+ "your site cannot be submitted to the browser preload list.",
108
+ id="security.W021",
109
+ )
110
+
111
+ W022 = Warning(
112
+ "You have not set the SECURE_REFERRER_POLICY setting. Without this, your "
113
+ "site will not send a Referrer-Policy header. You should consider "
114
+ "enabling this header to protect user privacy.",
115
+ id="security.W022",
116
+ )
117
+
118
+ E023 = Error(
119
+ "You have set the SECURE_REFERRER_POLICY setting to an invalid value.",
120
+ hint="Valid values are: {}.".format(", ".join(sorted(REFERRER_POLICY_VALUES))),
121
+ id="security.E023",
122
+ )
123
+
124
+ E024 = Error(
125
+ "You have set the SECURE_CROSS_ORIGIN_OPENER_POLICY setting to an invalid "
126
+ "value.",
127
+ hint="Valid values are: {}.".format(
128
+ ", ".join(sorted(CROSS_ORIGIN_OPENER_POLICY_VALUES)),
129
+ ),
130
+ id="security.E024",
131
+ )
132
+
133
+ W025 = Warning(SECRET_KEY_WARNING_MSG, id="security.W025")
134
+
135
+
136
+ def _security_middleware():
137
+ return "plain.middleware.security.SecurityMiddleware" in settings.MIDDLEWARE
138
+
139
+
140
+ def _xframe_middleware():
141
+ return (
142
+ "plain.middleware.clickjacking.XFrameOptionsMiddleware" in settings.MIDDLEWARE
143
+ )
144
+
145
+
146
+ @register(deploy=True)
147
+ def check_security_middleware(package_configs, **kwargs):
148
+ passed_check = _security_middleware()
149
+ return [] if passed_check else [W001]
150
+
151
+
152
+ @register(deploy=True)
153
+ def check_xframe_options_middleware(package_configs, **kwargs):
154
+ passed_check = _xframe_middleware()
155
+ return [] if passed_check else [W002]
156
+
157
+
158
+ @register(deploy=True)
159
+ def check_sts(package_configs, **kwargs):
160
+ passed_check = not _security_middleware() or settings.SECURE_HSTS_SECONDS
161
+ return [] if passed_check else [W004]
162
+
163
+
164
+ @register(deploy=True)
165
+ def check_sts_include_subdomains(package_configs, **kwargs):
166
+ passed_check = (
167
+ not _security_middleware()
168
+ or not settings.SECURE_HSTS_SECONDS
169
+ or settings.SECURE_HSTS_INCLUDE_SUBDOMAINS is True
170
+ )
171
+ return [] if passed_check else [W005]
172
+
173
+
174
+ @register(deploy=True)
175
+ def check_sts_preload(package_configs, **kwargs):
176
+ passed_check = (
177
+ not _security_middleware()
178
+ or not settings.SECURE_HSTS_SECONDS
179
+ or settings.SECURE_HSTS_PRELOAD is True
180
+ )
181
+ return [] if passed_check else [W021]
182
+
183
+
184
+ @register(deploy=True)
185
+ def check_content_type_nosniff(package_configs, **kwargs):
186
+ passed_check = (
187
+ not _security_middleware() or settings.SECURE_CONTENT_TYPE_NOSNIFF is True
188
+ )
189
+ return [] if passed_check else [W006]
190
+
191
+
192
+ @register(deploy=True)
193
+ def check_ssl_redirect(package_configs, **kwargs):
194
+ passed_check = not _security_middleware() or settings.SECURE_SSL_REDIRECT is True
195
+ return [] if passed_check else [W008]
196
+
197
+
198
+ def _check_secret_key(secret_key):
199
+ return (
200
+ len(set(secret_key)) >= SECRET_KEY_MIN_UNIQUE_CHARACTERS
201
+ and len(secret_key) >= SECRET_KEY_MIN_LENGTH
202
+ and not secret_key.startswith(SECRET_KEY_INSECURE_PREFIX)
203
+ )
204
+
205
+
206
+ @register(deploy=True)
207
+ def check_secret_key(package_configs, **kwargs):
208
+ try:
209
+ secret_key = settings.SECRET_KEY
210
+ except (ImproperlyConfigured, AttributeError):
211
+ passed_check = False
212
+ else:
213
+ passed_check = _check_secret_key(secret_key)
214
+ return [] if passed_check else [W009]
215
+
216
+
217
+ @register(deploy=True)
218
+ def check_secret_key_fallbacks(package_configs, **kwargs):
219
+ warnings = []
220
+ try:
221
+ fallbacks = settings.SECRET_KEY_FALLBACKS
222
+ except (ImproperlyConfigured, AttributeError):
223
+ warnings.append(Warning(W025.msg % "SECRET_KEY_FALLBACKS", id=W025.id))
224
+ else:
225
+ for index, key in enumerate(fallbacks):
226
+ if not _check_secret_key(key):
227
+ warnings.append(
228
+ Warning(W025.msg % f"SECRET_KEY_FALLBACKS[{index}]", id=W025.id)
229
+ )
230
+ return warnings
231
+
232
+
233
+ @register(deploy=True)
234
+ def check_debug(package_configs, **kwargs):
235
+ passed_check = not settings.DEBUG
236
+ return [] if passed_check else [W018]
237
+
238
+
239
+ @register(deploy=True)
240
+ def check_allowed_hosts(package_configs, **kwargs):
241
+ return [] if settings.ALLOWED_HOSTS else [W020]
242
+
243
+
244
+ @register(deploy=True)
245
+ def check_referrer_policy(package_configs, **kwargs):
246
+ if _security_middleware():
247
+ if settings.SECURE_REFERRER_POLICY is None:
248
+ return [W022]
249
+ # Support a comma-separated string or iterable of values to allow fallback.
250
+ if isinstance(settings.SECURE_REFERRER_POLICY, str):
251
+ values = {v.strip() for v in settings.SECURE_REFERRER_POLICY.split(",")}
252
+ else:
253
+ values = set(settings.SECURE_REFERRER_POLICY)
254
+ if not values <= REFERRER_POLICY_VALUES:
255
+ return [E023]
256
+ return []
257
+
258
+
259
+ @register(deploy=True)
260
+ def check_cross_origin_opener_policy(package_configs, **kwargs):
261
+ if (
262
+ _security_middleware()
263
+ and settings.SECURE_CROSS_ORIGIN_OPENER_POLICY is not None
264
+ and settings.SECURE_CROSS_ORIGIN_OPENER_POLICY
265
+ not in CROSS_ORIGIN_OPENER_POLICY_VALUES
266
+ ):
267
+ return [E024]
268
+ return []