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