1from __future__ import annotations
2
3import re
4from datetime import datetime
5from typing import TYPE_CHECKING
6
7from plain import models
8from plain.models import types
9
10if TYPE_CHECKING:
11 from plain.http import Request
12
13
14@models.register_model
15class Redirect(models.Model):
16 from_pattern: str = types.CharField(max_length=255)
17 to_pattern: str = types.CharField(max_length=255)
18 http_status: int = types.PositiveSmallIntegerField(
19 default=301
20 ) # Default to permanent - could be choices?
21 created_at: datetime = types.DateTimeField(auto_now_add=True)
22 updated_at: datetime = types.DateTimeField(auto_now=True)
23 order: int = types.PositiveSmallIntegerField(default=0)
24 enabled: bool = types.BooleanField(default=True)
25 is_regex: bool = types.BooleanField(default=False)
26
27 # query params?
28 # logged in or not? auth not required necessarily...
29 # headers?
30
31 query: models.QuerySet[Redirect] = models.QuerySet()
32
33 model_options = models.Options(
34 ordering=["order", "-created_at"],
35 indexes=[
36 models.Index(fields=["order"]),
37 models.Index(fields=["created_at"]),
38 ],
39 constraints=[
40 models.UniqueConstraint(
41 fields=["from_pattern"],
42 name="plainredirects_redirect_unique_from_pattern",
43 ),
44 ],
45 )
46
47 def __str__(self) -> str:
48 return f"{self.from_pattern}"
49
50 def matches_request(self, request: Request) -> bool:
51 """
52 Decide whether a request matches this Redirect,
53 automatically checking whether the pattern is path based or full URL based.
54 """
55
56 if self.from_pattern.startswith("http"):
57 # Full url with query params
58 url = request.build_absolute_uri()
59 else:
60 # Doesn't include query params or host
61 url = request.path
62
63 if self.is_regex:
64 return bool(re.match(self.from_pattern, url))
65 else:
66 return url == self.from_pattern
67
68 def get_redirect_url(self, request: Request) -> str:
69 if not self.is_regex:
70 return self.to_pattern
71
72 # Replace any regex groups in the to_pattern
73 if self.from_pattern.startswith("http"):
74 url = request.build_absolute_uri()
75 else:
76 url = request.path
77
78 return re.sub(self.from_pattern, self.to_pattern, url)
79
80
81@models.register_model
82class RedirectLog(models.Model):
83 redirect: Redirect = types.ForeignKeyField(Redirect, on_delete=models.CASCADE)
84
85 # The actuals that were used to redirect
86 from_url: str = types.URLField(max_length=512)
87 to_url: str = types.URLField(max_length=512)
88 http_status: int = types.PositiveSmallIntegerField(default=301)
89
90 # Request metadata
91 ip_address: str = types.GenericIPAddressField()
92 user_agent: str = types.CharField(required=False, max_length=512)
93 referrer: str = types.CharField(required=False, max_length=512)
94
95 created_at: datetime = types.DateTimeField(auto_now_add=True)
96
97 query: models.QuerySet[RedirectLog] = models.QuerySet()
98
99 model_options = models.Options(
100 ordering=["-created_at"],
101 indexes=[
102 models.Index(fields=["created_at"]),
103 ],
104 )
105
106 @classmethod
107 def from_redirect(cls, redirect: Redirect, request: Request) -> RedirectLog:
108 from_url = request.build_absolute_uri()
109 to_url = redirect.get_redirect_url(request)
110
111 if not to_url.startswith("http"):
112 to_url = request.build_absolute_uri(to_url)
113
114 if from_url == to_url:
115 raise ValueError("Redirecting to the same URL")
116
117 return cls.query.create(
118 redirect=redirect,
119 from_url=from_url,
120 to_url=to_url,
121 http_status=redirect.http_status,
122 ip_address=request.client_ip,
123 user_agent=request.headers.get("User-Agent", ""),
124 referrer=request.headers.get("Referer", ""),
125 )
126
127
128@models.register_model
129class NotFoundLog(models.Model):
130 url: str = types.URLField(max_length=512)
131
132 # Request metadata
133 ip_address: str = types.GenericIPAddressField()
134 user_agent: str = types.CharField(required=False, max_length=512)
135 referrer: str = types.CharField(required=False, max_length=512)
136
137 created_at: datetime = types.DateTimeField(auto_now_add=True)
138
139 query: models.QuerySet[NotFoundLog] = models.QuerySet()
140
141 model_options = models.Options(
142 ordering=["-created_at"],
143 indexes=[
144 models.Index(fields=["created_at"]),
145 ],
146 )
147
148 @classmethod
149 def from_request(cls, request: Request) -> NotFoundLog:
150 return cls.query.create(
151 url=request.build_absolute_uri(),
152 ip_address=request.client_ip,
153 user_agent=request.headers.get("User-Agent", ""),
154 referrer=request.headers.get("Referer", ""),
155 )