plain 0.4.1__py3-none-any.whl → 0.6.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/cli/cli.py CHANGED
@@ -14,7 +14,9 @@ from click.core import Command, Context
14
14
  import plain.runtime
15
15
  from plain import preflight
16
16
  from plain.assets.compile import compile_assets, get_compiled_path
17
+ from plain.exceptions import ImproperlyConfigured
17
18
  from plain.packages import packages
19
+ from plain.utils.crypto import get_random_string
18
20
 
19
21
  from .formatting import PlainContext
20
22
  from .packages import EntryPointGroup, InstalledPackagesGroup
@@ -406,6 +408,18 @@ def setting(setting_name):
406
408
  click.secho(f'Setting "{setting_name}" not found', fg="red")
407
409
 
408
410
 
411
+ @plain_cli.group()
412
+ def utils():
413
+ pass
414
+
415
+
416
+ @utils.command()
417
+ def generate_secret_key():
418
+ """Generate a new secret key"""
419
+ new_secret_key = get_random_string(50)
420
+ click.echo(new_secret_key)
421
+
422
+
409
423
  class AppCLIGroup(click.Group):
410
424
  """
411
425
  Loads app.cli if it exists as `plain app`
@@ -459,16 +473,31 @@ class PlainCommandCollection(click.CommandCollection):
459
473
  EntryPointGroup(),
460
474
  plain_cli,
461
475
  ]
462
- except Exception as e:
476
+ except ImproperlyConfigured as e:
463
477
  click.secho(
464
- f"Error setting up Plain CLI\n{e}",
478
+ str(e),
465
479
  fg="red",
466
480
  err=True,
467
481
  )
482
+
483
+ # None of these require the app to be setup
484
+ sources = [
485
+ EntryPointGroup(),
486
+ AppCLIGroup(),
487
+ plain_cli,
488
+ ]
489
+ except Exception as e:
468
490
  print("---")
469
491
  print(traceback.format_exc())
470
492
  print("---")
471
493
 
494
+ click.secho(
495
+ f"Error: {e}",
496
+ fg="red",
497
+ err=True,
498
+ )
499
+
500
+ # None of these require the app to be setup
472
501
  sources = [
473
502
  EntryPointGroup(),
474
503
  AppCLIGroup(),
plain/forms/fields.py CHANGED
@@ -14,7 +14,6 @@ from urllib.parse import urlsplit, urlunsplit
14
14
 
15
15
  from plain import validators
16
16
  from plain.exceptions import ValidationError
17
- from plain.runtime import settings
18
17
  from plain.utils import timezone
19
18
  from plain.utils.dateparse import parse_datetime, parse_duration
20
19
  from plain.utils.duration import duration_string
@@ -1001,7 +1000,7 @@ def from_current_timezone(value):
1001
1000
  When time zone support is enabled, convert naive datetimes
1002
1001
  entered in the current time zone to aware datetimes.
1003
1002
  """
1004
- if settings.USE_TZ and value is not None and timezone.is_naive(value):
1003
+ if value is not None and timezone.is_naive(value):
1005
1004
  current_timezone = timezone.get_current_timezone()
1006
1005
  try:
1007
1006
  if timezone._datetime_ambiguous_or_imaginary(value, current_timezone):
@@ -1025,6 +1024,6 @@ def to_current_timezone(value):
1025
1024
  When time zone support is enabled, convert aware datetimes
1026
1025
  to naive datetimes in the current time zone for display.
1027
1026
  """
1028
- if settings.USE_TZ and value is not None and timezone.is_aware(value):
1027
+ if value is not None and timezone.is_aware(value):
1029
1028
  return timezone.make_naive(value)
1030
1029
  return value
plain/forms/forms.py CHANGED
@@ -203,7 +203,8 @@ class BaseForm:
203
203
  self._errors[field].extend(error_list)
204
204
 
205
205
  # The field had an error, so removed it from the final data
206
- if field in self.cleaned_data:
206
+ # (we use getattr here so errors can be added to uncleaned forms)
207
+ if field in getattr(self, "cleaned_data", {}):
207
208
  del self.cleaned_data[field]
208
209
 
209
210
  def full_clean(self):
@@ -7,15 +7,11 @@ from plain.runtime import settings
7
7
  class SecurityMiddleware:
8
8
  def __init__(self, get_response):
9
9
  self.get_response = get_response
10
- self.sts_seconds = settings.SECURE_HSTS_SECONDS
11
- self.sts_include_subdomains = settings.SECURE_HSTS_INCLUDE_SUBDOMAINS
12
- self.sts_preload = settings.SECURE_HSTS_PRELOAD
13
- self.content_type_nosniff = settings.SECURE_CONTENT_TYPE_NOSNIFF
14
10
  self.redirect = settings.SECURE_SSL_REDIRECT
15
11
  self.redirect_host = settings.SECURE_SSL_HOST
16
12
  self.redirect_exempt = [re.compile(r) for r in settings.SECURE_REDIRECT_EXEMPT]
17
- self.referrer_policy = settings.SECURE_REFERRER_POLICY
18
- self.cross_origin_opener_policy = settings.SECURE_CROSS_ORIGIN_OPENER_POLICY
13
+
14
+ self.default_headers = settings.SECURE_DEFAULT_HEADERS
19
15
 
20
16
  def __call__(self, request):
21
17
  path = request.path.lstrip("/")
@@ -29,36 +25,7 @@ class SecurityMiddleware:
29
25
 
30
26
  response = self.get_response(request)
31
27
 
32
- if (
33
- self.sts_seconds
34
- and request.is_secure()
35
- and "Strict-Transport-Security" not in response
36
- ):
37
- sts_header = "max-age=%s" % self.sts_seconds
38
- if self.sts_include_subdomains:
39
- sts_header += "; includeSubDomains"
40
- if self.sts_preload:
41
- sts_header += "; preload"
42
- response.headers["Strict-Transport-Security"] = sts_header
43
-
44
- if self.content_type_nosniff:
45
- response.headers.setdefault("X-Content-Type-Options", "nosniff")
46
-
47
- if self.referrer_policy:
48
- # Support a comma-separated string or iterable of values to allow
49
- # fallback.
50
- response.headers.setdefault(
51
- "Referrer-Policy",
52
- ",".join(
53
- [v.strip() for v in self.referrer_policy.split(",")]
54
- if isinstance(self.referrer_policy, str)
55
- else self.referrer_policy
56
- ),
57
- )
28
+ for header, value in self.default_headers.items():
29
+ response.headers.setdefault(header, value)
58
30
 
59
- if self.cross_origin_opener_policy:
60
- response.setdefault(
61
- "Cross-Origin-Opener-Policy",
62
- self.cross_origin_opener_policy,
63
- )
64
31
  return response
@@ -1,23 +1,7 @@
1
1
  from plain.exceptions import ImproperlyConfigured
2
2
  from plain.runtime import settings
3
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
- }
4
+ from .. import Warning, register
21
5
 
22
6
  SECRET_KEY_INSECURE_PREFIX = "plain-insecure-"
23
7
  SECRET_KEY_MIN_LENGTH = 50
@@ -41,43 +25,6 @@ W001 = Warning(
41
25
  id="security.W001",
42
26
  )
43
27
 
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
28
  W008 = Warning(
82
29
  "Your SECURE_SSL_REDIRECT setting is not set to True. "
83
30
  "Unless your site should be available over both SSL and non-SSL "
@@ -102,34 +49,6 @@ W020 = Warning(
102
49
  id="security.W020",
103
50
  )
104
51
 
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
52
  W025 = Warning(SECRET_KEY_WARNING_MSG, id="security.W025")
134
53
 
135
54
 
@@ -137,58 +56,12 @@ def _security_middleware():
137
56
  return "plain.middleware.security.SecurityMiddleware" in settings.MIDDLEWARE
138
57
 
139
58
 
140
- def _xframe_middleware():
141
- return (
142
- "plain.middleware.clickjacking.XFrameOptionsMiddleware" in settings.MIDDLEWARE
143
- )
144
-
145
-
146
59
  @register(deploy=True)
147
60
  def check_security_middleware(package_configs, **kwargs):
148
61
  passed_check = _security_middleware()
149
62
  return [] if passed_check else [W001]
150
63
 
151
64
 
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
65
  @register(deploy=True)
193
66
  def check_ssl_redirect(package_configs, **kwargs):
194
67
  passed_check = not _security_middleware() or settings.SECURE_SSL_REDIRECT is True
@@ -239,30 +112,3 @@ def check_debug(package_configs, **kwargs):
239
112
  @register(deploy=True)
240
113
  def check_allowed_hosts(package_configs, **kwargs):
241
114
  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 []
plain/runtime/README.md CHANGED
@@ -59,7 +59,6 @@ MIDDLEWARE = [
59
59
  "plain.middleware.common.CommonMiddleware",
60
60
  "plain.csrf.middleware.CsrfViewMiddleware",
61
61
  "plain.auth.middleware.AuthenticationMiddleware",
62
- "plain.middleware.clickjacking.XFrameOptionsMiddleware",
63
62
  ]
64
63
 
65
64
  if DEBUG:
plain/runtime/__init__.py CHANGED
@@ -5,7 +5,7 @@ from pathlib import Path
5
5
 
6
6
  from dotenv import load_dotenv
7
7
 
8
- from .user_settings import LazySettings
8
+ from .user_settings import Settings
9
9
 
10
10
  try:
11
11
  __version__ = importlib.metadata.version("plain")
@@ -18,7 +18,7 @@ APP_PATH = Path.cwd() / "app"
18
18
 
19
19
 
20
20
  # from plain.runtime import settings
21
- settings = LazySettings()
21
+ settings = Settings()
22
22
 
23
23
 
24
24
  class AppPathNotFound(RuntimeError):
@@ -20,12 +20,9 @@ ALLOWED_HOSTS: list[str] = []
20
20
 
21
21
  # Local time zone for this installation. All choices can be found here:
22
22
  # https://en.wikipedia.org/wiki/List_of_tz_zones_by_name (although not all
23
- # systems may support all possibilities). When USE_TZ is True, this is
24
- # interpreted as the default user time zone.
25
- TIME_ZONE = "America/Chicago"
26
-
27
- # If you set this to True, Plain will use timezone-aware datetimes.
28
- USE_TZ = True
23
+ # systems may support all possibilities). This is interpreted as the default
24
+ # user time zone.
25
+ TIME_ZONE: str = "UTC"
29
26
 
30
27
  # Default charset to use for all Response objects, if a MIME type isn't
31
28
  # manually specified. It's used to construct the Content-Type header.
@@ -75,19 +72,6 @@ DATA_UPLOAD_MAX_NUMBER_FILES = 100
75
72
  # (i.e. "/tmp" on *nix systems).
76
73
  FILE_UPLOAD_TEMP_DIR = None
77
74
 
78
- # The numeric mode to set newly-uploaded files to. The value should be a mode
79
- # you'd pass directly to os.chmod; see
80
- # https://docs.python.org/library/os.html#files-and-directories.
81
- FILE_UPLOAD_PERMISSIONS = 0o644
82
-
83
- # The numeric mode to assign to newly-created directories, when uploading files.
84
- # The value should be a mode as you'd pass to os.chmod;
85
- # see https://docs.python.org/library/os.html#files-and-directories.
86
- FILE_UPLOAD_DIRECTORY_PERMISSIONS = None
87
-
88
- # Default X-Frame-Options header value
89
- X_FRAME_OPTIONS = "DENY"
90
-
91
75
  USE_X_FORWARDED_HOST = False
92
76
  USE_X_FORWARDED_PORT = False
93
77
 
@@ -114,14 +98,13 @@ MIDDLEWARE = [
114
98
  "plain.middleware.security.SecurityMiddleware",
115
99
  "plain.middleware.common.CommonMiddleware",
116
100
  "plain.csrf.middleware.CsrfViewMiddleware",
117
- "plain.middleware.clickjacking.XFrameOptionsMiddleware",
118
101
  ]
119
102
 
120
103
  ###########
121
104
  # SIGNING #
122
105
  ###########
123
106
 
124
- SIGNING_BACKEND = "plain.signing.TimestampSigner"
107
+ COOKIE_SIGNING_BACKEND = "plain.signing.TimestampSigner"
125
108
 
126
109
  ########
127
110
  # CSRF #
@@ -132,7 +115,7 @@ CSRF_COOKIE_NAME = "csrftoken"
132
115
  CSRF_COOKIE_AGE = 60 * 60 * 24 * 7 * 52
133
116
  CSRF_COOKIE_DOMAIN = None
134
117
  CSRF_COOKIE_PATH = "/"
135
- CSRF_COOKIE_SECURE = False
118
+ CSRF_COOKIE_SECURE = True
136
119
  CSRF_COOKIE_HTTPONLY = False
137
120
  CSRF_COOKIE_SAMESITE = "Lax"
138
121
  CSRF_HEADER_NAME = "HTTP_X_CSRFTOKEN"
@@ -170,15 +153,19 @@ SILENCED_PREFLIGHT_CHECKS = []
170
153
  #######################
171
154
  # SECURITY MIDDLEWARE #
172
155
  #######################
173
- SECURE_CONTENT_TYPE_NOSNIFF = True
174
- SECURE_CROSS_ORIGIN_OPENER_POLICY = "same-origin"
175
- SECURE_HSTS_INCLUDE_SUBDOMAINS = False
176
- SECURE_HSTS_PRELOAD = False
177
- SECURE_HSTS_SECONDS = 0
178
156
  SECURE_REDIRECT_EXEMPT = []
179
- SECURE_REFERRER_POLICY = "same-origin"
180
157
  SECURE_SSL_HOST = None
181
- SECURE_SSL_REDIRECT = False
158
+ SECURE_SSL_REDIRECT = True
159
+
160
+ SECURE_DEFAULT_HEADERS = {
161
+ # "Content-Security-Policy": "default-src 'self'",
162
+ # https://hstspreload.org/
163
+ # "Strict-Transport-Security": "max-age=31536000; includeSubDomains; preload",
164
+ "Cross-Origin-Opener-Policy": "same-origin",
165
+ "Referrer-Policy": "same-origin",
166
+ "X-Content-Type-Options": "nosniff",
167
+ "X-Frame-Options": "DENY",
168
+ }
182
169
 
183
170
  #############
184
171
  # Templates #
@@ -1,13 +1,5 @@
1
- """
2
- Settings and configuration for Plain.
3
-
4
- Read values from the module specified by the PLAIN_SETTINGS_MODULE environment
5
- variable, and then from plain.global_settings; see the global_settings.py
6
- for a list of all possible variables.
7
- """
8
1
  import importlib
9
2
  import json
10
- import logging
11
3
  import os
12
4
  import time
13
5
  import types
@@ -16,106 +8,256 @@ from pathlib import Path
16
8
 
17
9
  from plain.exceptions import ImproperlyConfigured
18
10
  from plain.packages import PackageConfig
19
- from plain.utils.functional import LazyObject, empty
20
11
 
21
12
  ENVIRONMENT_VARIABLE = "PLAIN_SETTINGS_MODULE"
22
13
  ENV_SETTINGS_PREFIX = "PLAIN_"
23
-
24
- logger = logging.getLogger("plain.runtime")
14
+ CUSTOM_SETTINGS_PREFIX = "APP_"
25
15
 
26
16
 
27
- class SettingsReference(str):
17
+ class Settings:
28
18
  """
29
- String subclass which references a current settings value. It's treated as
30
- the value in memory but serializes to a settings.NAME attribute reference.
19
+ Settings and configuration for Plain.
20
+
21
+ This class handles loading settings from the module specified by the
22
+ PLAIN_SETTINGS_MODULE environment variable, as well as from default settings,
23
+ environment variables, and explicit settings in the settings module.
24
+
25
+ Lazy initialization is implemented to defer loading until settings are first accessed.
31
26
  """
32
27
 
33
- def __new__(self, value, setting_name):
34
- return str.__new__(self, value)
28
+ def __init__(self, settings_module=None):
29
+ self._settings_module = settings_module
30
+ self._settings = {}
31
+ self._errors = [] # Collect configuration errors
32
+ self.configured = False
35
33
 
36
- def __init__(self, value, setting_name):
37
- self.setting_name = setting_name
34
+ def _setup(self):
35
+ if self.configured:
36
+ return
37
+ else:
38
+ self.configured = True
38
39
 
40
+ self._settings = {} # Maps setting names to SettingDefinition instances
39
41
 
40
- class LazySettings(LazyObject):
41
- """
42
- A lazy proxy for either global Plain settings or a custom settings object.
43
- The user can manually configure settings prior to using them. Otherwise,
44
- Plain uses the settings module pointed to by PLAIN_SETTINGS_MODULE.
45
- """
42
+ # Determine the settings module
43
+ if self._settings_module is None:
44
+ self._settings_module = os.environ.get(ENVIRONMENT_VARIABLE, "settings")
45
+
46
+ # First load the global settings from plain
47
+ self._load_module_settings(
48
+ importlib.import_module("plain.runtime.global_settings")
49
+ )
46
50
 
47
- def _setup(self, name=None):
48
- """
49
- Load the settings module pointed to by the environment variable. This
50
- is used the first time settings are needed, if the user hasn't
51
- configured settings manually.
52
- """
53
- settings_module = os.environ.get(ENVIRONMENT_VARIABLE, "settings")
54
- self._wrapped = Settings(settings_module)
51
+ # Import the user's settings module
52
+ try:
53
+ mod = importlib.import_module(self._settings_module)
54
+ except ImportError as e:
55
+ raise ImproperlyConfigured(
56
+ f"Could not import settings '{self._settings_module}': {e}"
57
+ )
55
58
 
56
- def __repr__(self):
57
- # Hardcode the class name as otherwise it yields 'Settings'.
58
- if self._wrapped is empty:
59
- return "<LazySettings [Unevaluated]>"
60
- return f'<LazySettings "{self._wrapped.SETTINGS_MODULE}">'
59
+ # Keep a reference to the settings.py module path
60
+ self.path = Path(mod.__file__).resolve()
61
+
62
+ # Load default settings from installed packages
63
+ self._load_default_settings(mod)
64
+ # Load environment settings
65
+ self._load_env_settings()
66
+ # Load explicit settings from the settings module
67
+ self._load_explicit_settings(mod)
68
+ # Check for any required settings that are missing
69
+ self._check_required_settings()
70
+ # Check for any collected errors
71
+ self._raise_errors_if_any()
72
+
73
+ def _load_module_settings(self, module):
74
+ annotations = getattr(module, "__annotations__", {})
75
+ settings = dir(module)
76
+
77
+ for setting in settings:
78
+ if setting.isupper():
79
+ if setting in self._settings:
80
+ self._errors.append(f"Duplicate setting '{setting}'.")
81
+ continue
82
+
83
+ setting_value = getattr(module, setting)
84
+ self._settings[setting] = SettingDefinition(
85
+ name=setting,
86
+ default_value=setting_value,
87
+ annotation=annotations.get(setting, None),
88
+ module=module,
89
+ )
90
+
91
+ # Store any annotations that didn't have a value (these are required settings)
92
+ for setting, annotation in annotations.items():
93
+ if setting not in self._settings:
94
+ self._settings[setting] = SettingDefinition(
95
+ name=setting,
96
+ default_value=None,
97
+ annotation=annotation,
98
+ module=module,
99
+ required=True,
100
+ )
101
+
102
+ def _load_default_settings(self, settings_module):
103
+ for entry in getattr(settings_module, "INSTALLED_PACKAGES", []):
104
+ try:
105
+ if isinstance(entry, PackageConfig):
106
+ app_settings = entry.module.default_settings
107
+ else:
108
+ app_settings = importlib.import_module(f"{entry}.default_settings")
109
+ except ModuleNotFoundError:
110
+ continue
111
+
112
+ self._load_module_settings(app_settings)
113
+
114
+ def _load_env_settings(self):
115
+ env_settings = {
116
+ k[len(ENV_SETTINGS_PREFIX) :]: v
117
+ for k, v in os.environ.items()
118
+ if k.startswith(ENV_SETTINGS_PREFIX) and k.isupper()
119
+ }
120
+ for setting, value in env_settings.items():
121
+ if setting in self._settings:
122
+ setting_def = self._settings[setting]
123
+ try:
124
+ parsed_value = _parse_env_value(value, setting_def.annotation)
125
+ setting_def.set_value(parsed_value, "env")
126
+ except ImproperlyConfigured as e:
127
+ self._errors.append(str(e))
128
+
129
+ def _load_explicit_settings(self, settings_module):
130
+ for setting in dir(settings_module):
131
+ if setting.isupper():
132
+ setting_value = getattr(settings_module, setting)
133
+
134
+ if setting in self._settings:
135
+ setting_def = self._settings[setting]
136
+ try:
137
+ setting_def.set_value(setting_value, "explicit")
138
+ except ImproperlyConfigured as e:
139
+ self._errors.append(str(e))
140
+ continue
141
+
142
+ elif setting.startswith(CUSTOM_SETTINGS_PREFIX):
143
+ # Accept custom settings prefixed with '{CUSTOM_SETTINGS_PREFIX}'
144
+ setting_def = SettingDefinition(
145
+ name=setting,
146
+ default_value=None,
147
+ annotation=None,
148
+ required=False,
149
+ )
150
+ try:
151
+ setting_def.set_value(setting_value, "explicit")
152
+ except ImproperlyConfigured as e:
153
+ self._errors.append(str(e))
154
+ continue
155
+ self._settings[setting] = setting_def
156
+ else:
157
+ # Collect unrecognized settings individually
158
+ self._errors.append(
159
+ f"Unknown setting '{setting}'. Custom settings must start with '{CUSTOM_SETTINGS_PREFIX}'."
160
+ )
161
+
162
+ if hasattr(time, "tzset") and self.TIME_ZONE:
163
+ zoneinfo_root = Path("/usr/share/zoneinfo")
164
+ zone_info_file = zoneinfo_root.joinpath(*self.TIME_ZONE.split("/"))
165
+ if zoneinfo_root.exists() and not zone_info_file.exists():
166
+ self._errors.append(
167
+ f"Invalid TIME_ZONE setting '{self.TIME_ZONE}'. Timezone file not found."
168
+ )
169
+ else:
170
+ os.environ["TZ"] = self.TIME_ZONE
171
+ time.tzset()
172
+
173
+ def _check_required_settings(self):
174
+ missing = [k for k, v in self._settings.items() if v.required and not v.is_set]
175
+ if missing:
176
+ self._errors.append(f"Missing required setting(s): {', '.join(missing)}.")
177
+
178
+ def _raise_errors_if_any(self):
179
+ if self._errors:
180
+ errors = ["- " + e for e in self._errors]
181
+ raise ImproperlyConfigured(
182
+ "Settings configuration errors:\n" + "\n".join(errors)
183
+ )
61
184
 
62
185
  def __getattr__(self, name):
63
- """Return the value of a setting and cache it in self.__dict__."""
64
- if (_wrapped := self._wrapped) is empty:
65
- self._setup(name)
66
- _wrapped = self._wrapped
67
- val = getattr(_wrapped, name)
186
+ # Avoid recursion by directly returning internal attributes
187
+ if not name.isupper():
188
+ return object.__getattribute__(self, name)
68
189
 
69
- # Special case some settings which require further modification.
70
- # This is done here for performance reasons so the modified value is cached.
71
- if name == "SECRET_KEY" and not val:
72
- raise ImproperlyConfigured("The SECRET_KEY setting must not be empty.")
190
+ self._setup()
73
191
 
74
- self.__dict__[name] = val
75
- return val
192
+ if name in self._settings:
193
+ return self._settings[name].value
194
+ else:
195
+ raise AttributeError(f"'Settings' object has no attribute '{name}'")
76
196
 
77
197
  def __setattr__(self, name, value):
78
- """
79
- Set the value of setting. Clear all cached values if _wrapped changes
80
- (@override_settings does this) or clear single values when set.
81
- """
82
- if name == "_wrapped":
83
- self.__dict__.clear()
198
+ # Handle internal attributes without recursion
199
+ if not name.isupper():
200
+ object.__setattr__(self, name, value)
84
201
  else:
85
- self.__dict__.pop(name, None)
86
- super().__setattr__(name, value)
87
-
88
- def __delattr__(self, name):
89
- """Delete a setting and clear it from cache if needed."""
90
- super().__delattr__(name)
91
- self.__dict__.pop(name, None)
202
+ if name in self._settings:
203
+ self._settings[name].set_value(value, "runtime")
204
+ self._raise_errors_if_any()
205
+ else:
206
+ object.__setattr__(self, name, value)
92
207
 
93
- @property
94
- def configured(self):
95
- """Return True if the settings have already been configured."""
96
- return self._wrapped is not empty
208
+ def __repr__(self):
209
+ if not self.configured:
210
+ return "<Settings [Unevaluated]>"
211
+ return f'<Settings "{self._settings_module}">'
212
+
213
+
214
+ def _parse_env_value(value, annotation):
215
+ if not annotation:
216
+ raise ImproperlyConfigured("Type hint required to set from environment.")
217
+
218
+ if annotation is bool:
219
+ # Special case for bools
220
+ return value.lower() in ("true", "1", "yes")
221
+ elif annotation is str:
222
+ return value
223
+ else:
224
+ # Parse other types using JSON
225
+ try:
226
+ return json.loads(value)
227
+ except json.JSONDecodeError as e:
228
+ raise ImproperlyConfigured(
229
+ f"Invalid JSON value for setting: {e.msg}"
230
+ ) from e
97
231
 
98
232
 
99
233
  class SettingDefinition:
100
- """Store some basic info about default settings and where they came from"""
234
+ """Store detailed information about settings."""
101
235
 
102
- def __init__(self, name, value, annotation, module, required=False):
236
+ def __init__(
237
+ self, name, default_value=None, annotation=None, module=None, required=False
238
+ ):
103
239
  self.name = name
104
- self.value = value
240
+ self.default_value = default_value
105
241
  self.annotation = annotation
106
242
  self.module = module
107
243
  self.required = required
244
+ self.value = default_value
245
+ self.source = "default" # 'default', 'env', 'explicit', or 'runtime'
246
+ self.is_set = False # Indicates if the value was set explicitly
108
247
 
109
- def __str__(self):
110
- return self.name
248
+ def set_value(self, value, source):
249
+ self.check_type(value)
250
+ self.value = value
251
+ self.source = source
252
+ self.is_set = True
111
253
 
112
254
  def check_type(self, obj):
113
255
  if not self.annotation:
114
256
  return
115
257
 
116
258
  if not SettingDefinition._is_instance_of_type(obj, self.annotation):
117
- raise ValueError(
118
- f"The {self.name} setting must be of type {self.annotation}"
259
+ raise ImproperlyConfigured(
260
+ f"'{self.name}': Expected type {self.annotation}, but got {type(obj)}."
119
261
  )
120
262
 
121
263
  @staticmethod
@@ -152,153 +294,20 @@ class SettingDefinition:
152
294
  for i, item in enumerate(value)
153
295
  )
154
296
 
155
- raise ValueError("Unsupported type hint: %s" % type_hint)
156
-
157
-
158
- class Settings:
159
- def __init__(self, settings_module):
160
- self._default_settings = {}
161
- self._explicit_settings = set()
162
-
163
- # First load the global settings from plain
164
- self._load_module_settings(
165
- importlib.import_module("plain.runtime.global_settings")
166
- )
167
-
168
- # store the settings module in case someone later cares
169
- self.SETTINGS_MODULE = settings_module
170
-
171
- mod = importlib.import_module(self.SETTINGS_MODULE)
172
-
173
- # Keep a reference to the settings.py module path
174
- # so we can find files next to it (assume it's at the app root)
175
- self.path = Path(mod.__file__).resolve()
176
-
177
- # First, get all the default_settings from the INSTALLED_PACKAGES and set those values
178
- self._load_default_settings(mod)
179
- # Second, look at the environment variables and overwrite with those
180
- self._load_env_settings()
181
- # Finally, load the explicit settings from the settings module
182
- self._load_explicit_settings(mod)
183
- # Check for any required settings that are missing
184
- self._check_required_settings()
185
-
186
- def _load_default_settings(self, settings_module):
187
- # Get INSTALLED_PACKAGES from mod,
188
- # then (without populating packages) do a check for default_settings in each
189
- # app and load those now too.
190
- for entry in getattr(settings_module, "INSTALLED_PACKAGES", []):
191
- try:
192
- if isinstance(entry, PackageConfig):
193
- app_settings = entry.module.default_settings
194
- else:
195
- app_settings = importlib.import_module(f"{entry}.default_settings")
196
- except ModuleNotFoundError:
197
- continue
198
-
199
- self._load_module_settings(app_settings)
200
-
201
- def _load_module_settings(self, module):
202
- annotations = getattr(module, "__annotations__", {})
203
- settings = dir(module)
204
-
205
- for setting in settings:
206
- if setting.isupper():
207
- if hasattr(self, setting):
208
- raise ImproperlyConfigured("The %s setting is duplicated" % setting)
297
+ raise ValueError(f"Unsupported type hint: {type_hint}")
209
298
 
210
- setting_value = getattr(module, setting)
211
-
212
- # Set a simple attr on the settings object
213
- setattr(self, setting, setting_value)
214
-
215
- # Store a more complex setting reference for more detail
216
- self._default_settings[setting] = SettingDefinition(
217
- name=setting,
218
- value=setting_value,
219
- annotation=annotations.get(setting, ""),
220
- module=module,
221
- )
222
-
223
- # Store any annotations that didn't have a value (these are required settings)
224
- for setting, annotation in annotations.items():
225
- if setting not in self._default_settings:
226
- self._default_settings[setting] = SettingDefinition(
227
- name=setting,
228
- value=None,
229
- annotation=annotation,
230
- module=module,
231
- required=True,
232
- )
233
-
234
- def _load_env_settings(self):
235
- env_settings = {
236
- k[len(ENV_SETTINGS_PREFIX) :]: v
237
- for k, v in os.environ.items()
238
- if k.startswith(ENV_SETTINGS_PREFIX)
239
- }
240
- logger.debug("Loading environment settings: %s", env_settings)
241
- for setting, value in env_settings.items():
242
- if setting not in self._default_settings:
243
- # Ignore anything not defined in the default settings
244
- continue
245
-
246
- default_setting = self._default_settings[setting]
247
- if not default_setting.annotation:
248
- raise ValueError(
249
- f"Setting {setting} needs a type hint to be set from the environment"
250
- )
251
-
252
- if default_setting.annotation is bool:
253
- # Special case for bools
254
- parsed_value = value.lower() in ("true", "1", "yes")
255
- elif default_setting.annotation is str:
256
- parsed_value = value
257
- else:
258
- # Anything besides a string will be parsed as JSON
259
- # (works for ints, lists, etc.)
260
- parsed_value = json.loads(value)
261
-
262
- default_setting.check_type(parsed_value)
263
-
264
- setattr(self, setting, parsed_value)
265
- self._explicit_settings.add(setting)
266
-
267
- def _load_explicit_settings(self, settings_module):
268
- for setting in dir(settings_module):
269
- if setting.isupper():
270
- setting_value = getattr(settings_module, setting)
271
-
272
- if setting in self._default_settings:
273
- self._default_settings[setting].check_type(setting_value)
274
-
275
- setattr(self, setting, setting_value)
276
- self._explicit_settings.add(setting)
299
+ def __str__(self):
300
+ return f"SettingDefinition(name={self.name}, value={self.value}, source={self.source})"
277
301
 
278
- if hasattr(time, "tzset") and self.TIME_ZONE:
279
- # When we can, attempt to validate the timezone. If we can't find
280
- # this file, no check happens and it's harmless.
281
- zoneinfo_root = Path("/usr/share/zoneinfo")
282
- zone_info_file = zoneinfo_root.joinpath(*self.TIME_ZONE.split("/"))
283
- if zoneinfo_root.exists() and not zone_info_file.exists():
284
- raise ValueError("Incorrect timezone setting: %s" % self.TIME_ZONE)
285
- # Move the time zone info into os.environ. See ticket #2315 for why
286
- # we don't do this unconditionally (breaks Windows).
287
- os.environ["TZ"] = self.TIME_ZONE
288
- time.tzset()
289
302
 
290
- def _check_required_settings(self):
291
- # Required settings have to be explicitly defined (there is no default)
292
- # so we can check whether they have been set by the user
293
- required_settings = {k for k, v in self._default_settings.items() if v.required}
294
- if missing := required_settings - self._explicit_settings:
295
- raise ImproperlyConfigured(
296
- "The following settings are required: %s" % ", ".join(missing)
297
- )
303
+ class SettingsReference(str):
304
+ """
305
+ String subclass which references a current settings value. It's treated as
306
+ the value in memory but serializes to a settings.NAME attribute reference.
307
+ """
298
308
 
299
- # Seems like this could almost be removed
300
- def is_overridden(self, setting):
301
- return setting in self._explicit_settings
309
+ def __new__(self, value, setting_name):
310
+ return str.__new__(self, value)
302
311
 
303
- def __repr__(self):
304
- return f'<{self.__class__.__name__} "{self.SETTINGS_MODULE}">'
312
+ def __init__(self, value, setting_name):
313
+ self.setting_name = setting_name
plain/signing.py CHANGED
@@ -37,12 +37,10 @@ import base64
37
37
  import datetime
38
38
  import json
39
39
  import time
40
- import warnings
41
40
  import zlib
42
41
 
43
42
  from plain.runtime import settings
44
43
  from plain.utils.crypto import constant_time_compare, salted_hmac
45
- from plain.utils.deprecation import RemovedInDjango51Warning
46
44
  from plain.utils.encoding import force_bytes
47
45
  from plain.utils.module_loading import import_string
48
46
  from plain.utils.regex_helper import _lazy_re_compile
@@ -109,7 +107,7 @@ def _cookie_signer_key(key):
109
107
 
110
108
 
111
109
  def get_cookie_signer(salt="plain.signing.get_cookie_signer"):
112
- Signer = import_string(settings.SIGNING_BACKEND)
110
+ Signer = import_string(settings.COOKIE_SIGNING_BACKEND)
113
111
  return Signer(
114
112
  key=_cookie_signer_key(settings.SECRET_KEY),
115
113
  fallback_keys=map(_cookie_signer_key, settings.SECRET_KEY_FALLBACKS),
@@ -177,17 +175,13 @@ def loads(
177
175
 
178
176
 
179
177
  class Signer:
180
- # RemovedInDjango51Warning: When the deprecation ends, replace with:
181
- # def __init__(
182
- # self, *, key=None, sep=":", salt=None, algorithm=None, fallback_keys=None
183
- # ):
184
178
  def __init__(
185
179
  self,
186
- *args,
180
+ *,
187
181
  key=None,
188
182
  sep=":",
189
183
  salt=None,
190
- algorithm=None,
184
+ algorithm="sha256",
191
185
  fallback_keys=None,
192
186
  ):
193
187
  self.key = key or settings.SECRET_KEY
@@ -198,20 +192,8 @@ class Signer:
198
192
  )
199
193
  self.sep = sep
200
194
  self.salt = salt or f"{self.__class__.__module__}.{self.__class__.__name__}"
201
- self.algorithm = algorithm or "sha256"
202
- # RemovedInDjango51Warning.
203
- if args:
204
- warnings.warn(
205
- f"Passing positional arguments to {self.__class__.__name__} is "
206
- f"deprecated.",
207
- RemovedInDjango51Warning,
208
- stacklevel=2,
209
- )
210
- for arg, attr in zip(
211
- args, ["key", "sep", "salt", "algorithm", "fallback_keys"]
212
- ):
213
- if arg or attr == "sep":
214
- setattr(self, attr, arg)
195
+ self.algorithm = algorithm
196
+
215
197
  if _SEP_UNSAFE.match(self.sep):
216
198
  raise ValueError(
217
199
  "Unsafe Signer separator: %r (cannot be empty or consist of "
plain/utils/timezone.py CHANGED
@@ -138,27 +138,6 @@ class override(ContextDecorator):
138
138
  _active.value = self.old_timezone
139
139
 
140
140
 
141
- # Templates
142
-
143
-
144
- def template_localtime(value, use_tz=None):
145
- """
146
- Check if value is a datetime and converts it to local time if necessary.
147
-
148
- If use_tz is provided and is not None, that will force the value to
149
- be converted (or not), overriding the value of settings.USE_TZ.
150
-
151
- This function is designed for use by the template engine.
152
- """
153
- should_convert = (
154
- isinstance(value, datetime)
155
- and (settings.USE_TZ if use_tz is None else use_tz)
156
- and not is_naive(value)
157
- and getattr(value, "convert_to_local_time", True)
158
- )
159
- return localtime(value) if should_convert else value
160
-
161
-
162
141
  # Utilities
163
142
 
164
143
 
@@ -184,9 +163,9 @@ def localtime(value=None, timezone=None):
184
163
 
185
164
  def now():
186
165
  """
187
- Return an aware or naive datetime.datetime, depending on settings.USE_TZ.
166
+ Return a timezone aware datetime.
188
167
  """
189
- return datetime.now(tz=timezone.utc if settings.USE_TZ else None)
168
+ return datetime.now(tz=timezone.utc)
190
169
 
191
170
 
192
171
  # By design, these four functions don't perform any checks on their arguments.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: plain
3
- Version: 0.4.1
3
+ Version: 0.6.0
4
4
  Summary: A web framework for building products with Python.
5
5
  Author: Dave Gaeddert
6
6
  Author-email: dave.gaeddert@dropseed.dev
@@ -9,7 +9,7 @@ plain/assets/urls.py,sha256=ZTIoM1Zq35JaXZ3wFhXhfGa7VoITDNlH9i5RS0R5xow,933
9
9
  plain/assets/views.py,sha256=dhjIpMu0GDR_VGbXM90_6RnC84C2C4bFv1RxDVklGBk,9173
10
10
  plain/cli/README.md,sha256=xjr1K-sIMTi5OWxdxL--O7aoo16Pd1xdawIZtz6BL7Q,2464
11
11
  plain/cli/__init__.py,sha256=9ByBOIdM8DebChjNz-RH2atdz4vWe8somlwNEsbhwh4,40
12
- plain/cli/cli.py,sha256=z2BoAdb5IF_WpPdbSsS1lc2GMsCOrb2y-ZgUfa6NmaE,14416
12
+ plain/cli/cli.py,sha256=myxXjRRLt8GQdJ4m3wOgW383D4Z1t1v07PABU0IQ6RM,15095
13
13
  plain/cli/formatting.py,sha256=1hZH13y1qwHcU2K2_Na388nw9uvoeQH8LrWL-O9h8Yc,2207
14
14
  plain/cli/packages.py,sha256=69VH1bIi1-5N5l2jlBcR5EP0pt-v16sPar9arO3gCSE,2052
15
15
  plain/cli/print.py,sha256=XraUYrgODOJquIiEv78wSCYGRBplHXtXSS9QtFG5hqY,217
@@ -23,8 +23,8 @@ plain/forms/README.md,sha256=fglB9MmHiEgfGGdZmcRstNl6eYaFljrElu2mzapK52M,377
23
23
  plain/forms/__init__.py,sha256=UxqPwB8CiYPCQdHmUc59jadqaXqDmXBH8y4bt9vTPms,226
24
24
  plain/forms/boundfield.py,sha256=LhydhCVR0okrli0-QBMjGjAJ8-06gTCXVEaBZhBouQk,1741
25
25
  plain/forms/exceptions.py,sha256=XCLDRl5snIEDu5-8mLB0NnU_tegcBfyIHMiJxqvbxnc,164
26
- plain/forms/fields.py,sha256=nyL1-NLAIt2jvEhGt4VwUy7xsXrVuU_wMuMzRXVhleA,35178
27
- plain/forms/forms.py,sha256=QgIV4JDLXzM8wfLYVZUUNH5kmJlQYQVG6UAENrxFIBo,10357
26
+ plain/forms/fields.py,sha256=86ZE9jac6Zyg5vKsYGgyOUOIQLKxO--UomGXwA65tk4,35103
27
+ plain/forms/forms.py,sha256=-EcS2QVpAy4H95Y-RL108LnWnHLSyCGgEnUCdIIXnjg,10451
28
28
  plain/http/README.md,sha256=00zLFQ-FPjYXu3A8QsLhCCXxaT0ImvI5I-8xd3dp8WA,7
29
29
  plain/http/__init__.py,sha256=DIsDRbBsCGa4qZgq-fUuQS0kkxfbTU_3KpIM9VvH04w,1067
30
30
  plain/http/cookie.py,sha256=11FnSG3Plo6T3jZDbPoCw7SKh9ExdBio3pTmIO03URg,597
@@ -53,10 +53,9 @@ plain/logs/loggers.py,sha256=iz9SYcwP9w5QAuwpULl48SFkVyJuuMoQ_fdLgdCHpNg,2121
53
53
  plain/logs/utils.py,sha256=9UzdCCQXJinGDs71Ngw297mlWkhgZStSd67ya4NOW98,1257
54
54
  plain/middleware/README.md,sha256=MgiLHwAfP8ooBSlDi1JhTwIHMlwphOqAkeWglYRbe8s,52
55
55
  plain/middleware/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
56
- plain/middleware/clickjacking.py,sha256=MJOHWSDqJB8K6YE6XTh34uyr2LNKuE9XsywZRRsljFk,1764
57
56
  plain/middleware/common.py,sha256=-YySkYUyaRujYA5Yg7GRD3xFjlQOZpeJP1Stpt6pias,3631
58
57
  plain/middleware/gzip.py,sha256=2NogLO6hPxVc3otxkhMDl7-r2Zw3vcIkAP29fx4j2eU,2383
59
- plain/middleware/security.py,sha256=r9UatFEaKVL1eZ5AxAuVX9uf5eLKwImEZmjL2t1slaY,2477
58
+ plain/middleware/security.py,sha256=WZRn5F9qx33wFTqh4CkBEtHrTuyr7RCt4Gwq4W2mBgE,1043
60
59
  plain/packages/README.md,sha256=Vq1Nw3mmEmZ2IriQavuVi4BjcQC2nb8k7YIbnm8QjIg,799
61
60
  plain/packages/__init__.py,sha256=DnHN1wwHXiXib4Y9BV__x9WrbUaTovoTIxW-tVyScTU,106
62
61
  plain/packages/config.py,sha256=6Vdf1TEQllZkkEvK0WK__zHJYT9nxmS3EyYrbuq0GkM,11201
@@ -68,19 +67,19 @@ plain/preflight/files.py,sha256=wbHCNgps7o1c1zQNBd8FDCaVaqX90UwuvLgEQ_DbUpY,510
68
67
  plain/preflight/messages.py,sha256=u0oc7q7YmBlKYJRcF5SQpzncfOkEzDhZTcpyclQDfHg,2427
69
68
  plain/preflight/registry.py,sha256=ZpxnZPIklXuT8xZVTxCUp_IER3zhd7DdfsmqIpAbLj4,2306
70
69
  plain/preflight/security/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
71
- plain/preflight/security/base.py,sha256=MjY1HXfUD4fsGz1kGs0eI2hpoSmD57x_30CMbLpw3h4,8445
70
+ plain/preflight/security/base.py,sha256=EY9rgXi8qdzLY1mMq9lMqYJmIV2OhN66Vt96meblxoE,3541
72
71
  plain/preflight/security/csrf.py,sha256=EZy_DkVqc1kUmBA-UbNmhVsKhRINfmqgWSRlatKy5AA,1237
73
72
  plain/preflight/urls.py,sha256=O4PQ_v205VA2872fQlhPfxaihDDRCsVp0ZVKQ92aX4k,3019
74
- plain/runtime/README.md,sha256=F5USETg445RX1RxC13y_1NrbOs3GHQI2Jl9idGdVFnk,2270
75
- plain/runtime/__init__.py,sha256=jKak9tK8MfcGNYOzqCOEmwSRAGOoji8r7e2IAwOymzE,1392
76
- plain/runtime/global_settings.py,sha256=PrJ5IAPk1nEr8U_XM3gLrItIVA4v2ig1t1bx_fV0NPE,6118
77
- plain/runtime/user_settings.py,sha256=3016w7YiOkG85L1Y5BsobgxgCOAaRbZzFaBadzmHSZg,11315
73
+ plain/runtime/README.md,sha256=wR0XrWAb4FGnOON2-O9ySSZCSMZmPkKGx-4DQXd_2h4,2209
74
+ plain/runtime/__init__.py,sha256=vIh77lL4e5CoQZz-HxvXeJL5329s8VfPFpNxx9rHgxs,1384
75
+ plain/runtime/global_settings.py,sha256=cVwGcdpDFf2OoYeGD3Lfv-mxGF48aKSGzZlLKwp8UKI,5566
76
+ plain/runtime/user_settings.py,sha256=JhxmCCOmEMk0QHh82l5iTpEie-UdZh13aXGsLhE5PBw,11255
78
77
  plain/signals/README.md,sha256=cd3tKEgH-xc88CUWyDxl4-qv-HBXx8VT32BXVwA5azA,230
79
78
  plain/signals/__init__.py,sha256=eAs0kLqptuP6I31dWXeAqRNji3svplpAV4Ez6ktjwXM,131
80
79
  plain/signals/dispatch/__init__.py,sha256=FzEygqV9HsM6gopio7O2Oh_X230nA4d5Q9s0sUjMq0E,292
81
80
  plain/signals/dispatch/dispatcher.py,sha256=VxSlqn9PCOTghPPJLOqZPs6FNQZfV2BJpMfFMSg6Dtc,11531
82
81
  plain/signals/dispatch/license.txt,sha256=o9EhDhsC4Q5HbmD-IfNGVTEkXtNE33r5rIt3lleJ8gc,1727
83
- plain/signing.py,sha256=rCsizsluW-lt8ihwicf19kU-sOewMvkIbeI9MrFNwOw,9491
82
+ plain/signing.py,sha256=V6A6PTDYWekuwtQRI1iFD8dud5OHPZTv4EkeoZEHoXo,8737
84
83
  plain/templates/README.md,sha256=VfA2HmrklG5weE1md85q9g84cWnMBEiXAynKzM7S1Sk,464
85
84
  plain/templates/__init__.py,sha256=Jh1jit55UR4dRpklQ6qAN2ixzYZBVoDi0AOdfD4Nh4E,106
86
85
  plain/templates/core.py,sha256=iw58EAmyyv8N5HDA-Sq4-fLgz_qx8v8WJfurgR116jw,625
@@ -129,7 +128,7 @@ plain/utils/safestring.py,sha256=SHGhpbX6FFDKSYOY9zYAgAQX0g0exzRba7dM2bJalWs,187
129
128
  plain/utils/termcolors.py,sha256=78MimQMp4Etoh1X1lokOJ6ucxErHtg8z9rxeTtV5nhk,7394
130
129
  plain/utils/text.py,sha256=QxhJsk_4VrNVUtwwo0DXGTMHJ1x_hrKOqJOxlPB33qc,16596
131
130
  plain/utils/timesince.py,sha256=essdb0XWBKWmKtIprs-4rO0qKTtsFqZ0Fwn-RTDyhOc,4758
132
- plain/utils/timezone.py,sha256=tFd4NfQNwTiHKExNxTc0wWKUpDZximHlcN0Ykv-wQ6k,6910
131
+ plain/utils/timezone.py,sha256=AZ7lcmUjofUTfQUb08pHXu0u7TDuPJpMRB5lgvE4E0w,6212
133
132
  plain/utils/tree.py,sha256=wdWzmfsgc26YDF2wxhAY3yVxXTixQYqYDKE9mL3L3ZY,4383
134
133
  plain/validators.py,sha256=L9v9KtTe4iZhZVramZdKGf33R5Tt95FCdg2AJD2-2n0,19963
135
134
  plain/views/README.md,sha256=qndsXKyNMnipPlLaAvgQeGxqXknNQwlFh31Yxk8rHp8,5994
@@ -143,8 +142,8 @@ plain/views/objects.py,sha256=9QBYyb8PgkRirXCQ8-Pms4_yMzP37dfeL30hWRYmtZg,7909
143
142
  plain/views/redirect.py,sha256=KLnlktzK6ZNMTlaEiZpMKQMEP5zeTgGLJ9BIkIJfwBo,1733
144
143
  plain/views/templates.py,sha256=nF9CcdhhjAyp3LB0RrSYnBaHpHzMfPSw719RCdcXk7o,2007
145
144
  plain/wsgi.py,sha256=R6k5FiAElvGDApEbMPTT0MPqSD7n2e2Az5chQqJZU0I,236
146
- plain-0.4.1.dist-info/LICENSE,sha256=m0D5O7QoH9l5Vz_rrX_9r-C8d9UNr_ciK6Qwac7o6yo,3175
147
- plain-0.4.1.dist-info/METADATA,sha256=l7Q6qWK98LKLVsoQ4Fr5Apb5YimjLkIzhUwAO4PSjPE,2716
148
- plain-0.4.1.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
149
- plain-0.4.1.dist-info/entry_points.txt,sha256=7O1RZTmMasKYB73bfqQcTwIhsXo7RjEIKv2WbtTtOIM,39
150
- plain-0.4.1.dist-info/RECORD,,
145
+ plain-0.6.0.dist-info/LICENSE,sha256=m0D5O7QoH9l5Vz_rrX_9r-C8d9UNr_ciK6Qwac7o6yo,3175
146
+ plain-0.6.0.dist-info/METADATA,sha256=QbORBVO36VHvPSJS5waroaoHUkGHP8wZ5Q22GO1ocaM,2716
147
+ plain-0.6.0.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
148
+ plain-0.6.0.dist-info/entry_points.txt,sha256=7O1RZTmMasKYB73bfqQcTwIhsXo7RjEIKv2WbtTtOIM,39
149
+ plain-0.6.0.dist-info/RECORD,,
@@ -1,52 +0,0 @@
1
- """
2
- Clickjacking Protection Middleware.
3
-
4
- This module provides a middleware that implements protection against a
5
- malicious site loading resources from your site in a hidden frame.
6
- """
7
-
8
- from plain.runtime import settings
9
-
10
-
11
- class XFrameOptionsMiddleware:
12
- """
13
- Set the X-Frame-Options HTTP header in HTTP responses.
14
-
15
- Do not set the header if it's already set or if the response contains
16
- a xframe_options_exempt value set to True.
17
-
18
- By default, set the X-Frame-Options header to 'DENY', meaning the response
19
- cannot be displayed in a frame, regardless of the site attempting to do so.
20
- To enable the response to be loaded on a frame within the same site, set
21
- X_FRAME_OPTIONS in your project's Plain settings to 'SAMEORIGIN'.
22
- """
23
-
24
- def __init__(self, get_response):
25
- self.get_response = get_response
26
-
27
- def __call__(self, request):
28
- response = self.get_response(request)
29
-
30
- # Don't set it if it's already in the response
31
- if response.get("X-Frame-Options") is not None:
32
- return response
33
-
34
- # Don't set it if they used @xframe_options_exempt
35
- if getattr(response, "xframe_options_exempt", False):
36
- return response
37
-
38
- response.headers["X-Frame-Options"] = self.get_xframe_options_value(
39
- request,
40
- response,
41
- )
42
- return response
43
-
44
- def get_xframe_options_value(self, request, response):
45
- """
46
- Get the value to set for the X_FRAME_OPTIONS header. Use the value from
47
- the X_FRAME_OPTIONS setting, or 'DENY' if not set.
48
-
49
- This method can be overridden if needed, allowing it to vary based on
50
- the request or response.
51
- """
52
- return getattr(settings, "X_FRAME_OPTIONS", "DENY").upper()
File without changes
File without changes