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.
- plain/README.md +33 -0
- plain/__main__.py +5 -0
- plain/assets/README.md +56 -0
- plain/assets/__init__.py +6 -0
- plain/assets/finders.py +233 -0
- plain/assets/preflight.py +14 -0
- plain/assets/storage.py +916 -0
- plain/assets/utils.py +52 -0
- plain/assets/whitenoise/__init__.py +5 -0
- plain/assets/whitenoise/base.py +259 -0
- plain/assets/whitenoise/compress.py +189 -0
- plain/assets/whitenoise/media_types.py +137 -0
- plain/assets/whitenoise/middleware.py +197 -0
- plain/assets/whitenoise/responders.py +286 -0
- plain/assets/whitenoise/storage.py +178 -0
- plain/assets/whitenoise/string_utils.py +13 -0
- plain/cli/README.md +123 -0
- plain/cli/__init__.py +3 -0
- plain/cli/cli.py +439 -0
- plain/cli/formatting.py +61 -0
- plain/cli/packages.py +73 -0
- plain/cli/print.py +9 -0
- plain/cli/startup.py +33 -0
- plain/csrf/README.md +3 -0
- plain/csrf/middleware.py +466 -0
- plain/csrf/views.py +10 -0
- plain/debug.py +23 -0
- plain/exceptions.py +242 -0
- plain/forms/README.md +14 -0
- plain/forms/__init__.py +8 -0
- plain/forms/boundfield.py +58 -0
- plain/forms/exceptions.py +11 -0
- plain/forms/fields.py +1030 -0
- plain/forms/forms.py +297 -0
- plain/http/README.md +1 -0
- plain/http/__init__.py +51 -0
- plain/http/cookie.py +20 -0
- plain/http/multipartparser.py +743 -0
- plain/http/request.py +754 -0
- plain/http/response.py +719 -0
- plain/internal/__init__.py +0 -0
- plain/internal/files/README.md +3 -0
- plain/internal/files/__init__.py +3 -0
- plain/internal/files/base.py +161 -0
- plain/internal/files/locks.py +127 -0
- plain/internal/files/move.py +102 -0
- plain/internal/files/temp.py +79 -0
- plain/internal/files/uploadedfile.py +150 -0
- plain/internal/files/uploadhandler.py +254 -0
- plain/internal/files/utils.py +78 -0
- plain/internal/handlers/__init__.py +0 -0
- plain/internal/handlers/base.py +133 -0
- plain/internal/handlers/exception.py +145 -0
- plain/internal/handlers/wsgi.py +216 -0
- plain/internal/legacy/__init__.py +0 -0
- plain/internal/legacy/__main__.py +12 -0
- plain/internal/legacy/management/__init__.py +414 -0
- plain/internal/legacy/management/base.py +692 -0
- plain/internal/legacy/management/color.py +113 -0
- plain/internal/legacy/management/commands/__init__.py +0 -0
- plain/internal/legacy/management/commands/collectstatic.py +297 -0
- plain/internal/legacy/management/sql.py +67 -0
- plain/internal/legacy/management/utils.py +175 -0
- plain/json.py +40 -0
- plain/logs/README.md +24 -0
- plain/logs/__init__.py +5 -0
- plain/logs/configure.py +39 -0
- plain/logs/loggers.py +74 -0
- plain/logs/utils.py +46 -0
- plain/middleware/README.md +3 -0
- plain/middleware/__init__.py +0 -0
- plain/middleware/clickjacking.py +52 -0
- plain/middleware/common.py +87 -0
- plain/middleware/gzip.py +64 -0
- plain/middleware/security.py +64 -0
- plain/packages/README.md +41 -0
- plain/packages/__init__.py +4 -0
- plain/packages/config.py +259 -0
- plain/packages/registry.py +438 -0
- plain/paginator.py +187 -0
- plain/preflight/README.md +3 -0
- plain/preflight/__init__.py +38 -0
- plain/preflight/compatibility/__init__.py +0 -0
- plain/preflight/compatibility/django_4_0.py +20 -0
- plain/preflight/files.py +19 -0
- plain/preflight/messages.py +88 -0
- plain/preflight/registry.py +72 -0
- plain/preflight/security/__init__.py +0 -0
- plain/preflight/security/base.py +268 -0
- plain/preflight/security/csrf.py +40 -0
- plain/preflight/urls.py +117 -0
- plain/runtime/README.md +75 -0
- plain/runtime/__init__.py +61 -0
- plain/runtime/global_settings.py +199 -0
- plain/runtime/user_settings.py +353 -0
- plain/signals/README.md +14 -0
- plain/signals/__init__.py +5 -0
- plain/signals/dispatch/__init__.py +9 -0
- plain/signals/dispatch/dispatcher.py +320 -0
- plain/signals/dispatch/license.txt +35 -0
- plain/signing.py +299 -0
- plain/templates/README.md +20 -0
- plain/templates/__init__.py +6 -0
- plain/templates/core.py +24 -0
- plain/templates/jinja/README.md +227 -0
- plain/templates/jinja/__init__.py +22 -0
- plain/templates/jinja/defaults.py +119 -0
- plain/templates/jinja/extensions.py +39 -0
- plain/templates/jinja/filters.py +28 -0
- plain/templates/jinja/globals.py +19 -0
- plain/test/README.md +3 -0
- plain/test/__init__.py +16 -0
- plain/test/client.py +985 -0
- plain/test/utils.py +255 -0
- plain/urls/README.md +3 -0
- plain/urls/__init__.py +40 -0
- plain/urls/base.py +118 -0
- plain/urls/conf.py +94 -0
- plain/urls/converters.py +66 -0
- plain/urls/exceptions.py +9 -0
- plain/urls/resolvers.py +731 -0
- plain/utils/README.md +3 -0
- plain/utils/__init__.py +0 -0
- plain/utils/_os.py +52 -0
- plain/utils/cache.py +327 -0
- plain/utils/connection.py +84 -0
- plain/utils/crypto.py +76 -0
- plain/utils/datastructures.py +345 -0
- plain/utils/dateformat.py +329 -0
- plain/utils/dateparse.py +154 -0
- plain/utils/dates.py +76 -0
- plain/utils/deconstruct.py +54 -0
- plain/utils/decorators.py +90 -0
- plain/utils/deprecation.py +6 -0
- plain/utils/duration.py +44 -0
- plain/utils/email.py +12 -0
- plain/utils/encoding.py +235 -0
- plain/utils/functional.py +456 -0
- plain/utils/hashable.py +26 -0
- plain/utils/html.py +401 -0
- plain/utils/http.py +374 -0
- plain/utils/inspect.py +73 -0
- plain/utils/ipv6.py +46 -0
- plain/utils/itercompat.py +8 -0
- plain/utils/module_loading.py +69 -0
- plain/utils/regex_helper.py +353 -0
- plain/utils/safestring.py +72 -0
- plain/utils/termcolors.py +221 -0
- plain/utils/text.py +518 -0
- plain/utils/timesince.py +138 -0
- plain/utils/timezone.py +244 -0
- plain/utils/tree.py +126 -0
- plain/validators.py +603 -0
- plain/views/README.md +268 -0
- plain/views/__init__.py +18 -0
- plain/views/base.py +107 -0
- plain/views/csrf.py +24 -0
- plain/views/errors.py +25 -0
- plain/views/exceptions.py +4 -0
- plain/views/forms.py +76 -0
- plain/views/objects.py +229 -0
- plain/views/redirect.py +72 -0
- plain/views/templates.py +66 -0
- plain/wsgi.py +11 -0
- plain-0.1.0.dist-info/LICENSE +85 -0
- plain-0.1.0.dist-info/METADATA +51 -0
- plain-0.1.0.dist-info/RECORD +169 -0
- plain-0.1.0.dist-info/WHEEL +4 -0
- 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,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
|
plain/preflight/files.py
ADDED
|
@@ -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 []
|