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 ]
35 constraints = [
36 models.UniqueConstraint(
37 fields=["from_pattern"],
38 name="plainredirects_redirect_unique_from_pattern",
39 ),
40 ]
41
42 def __str__(self):
43 return f"{self.from_pattern}"
44
45 def matches_request(self, request):
46 """
47 Decide whether a request matches this Redirect,
48 automatically checking whether the pattern is path based or full URL based.
49 """
50
51 if self.from_pattern.startswith("http"):
52 # Full url with query params
53 url = request.build_absolute_uri()
54 else:
55 # Doesn't include query params or host
56 url = request.path
57
58 if self.is_regex:
59 return re.match(self.from_pattern, url)
60 else:
61 return url == self.from_pattern
62
63 def get_redirect_url(self, request):
64 if not self.is_regex:
65 return self.to_pattern
66
67 # Replace any regex groups in the to_pattern
68 if self.from_pattern.startswith("http"):
69 url = request.build_absolute_uri()
70 else:
71 url = request.path
72
73 return re.sub(self.from_pattern, self.to_pattern, url)
74
75
76@models.register_model
77class RedirectLog(models.Model):
78 redirect = models.ForeignKey(Redirect, on_delete=models.CASCADE)
79
80 # The actuals that were used to redirect
81 from_url = models.URLField(max_length=512)
82 to_url = models.URLField(max_length=512)
83 http_status = models.PositiveSmallIntegerField(default=301)
84
85 # Request metadata
86 ip_address = models.GenericIPAddressField()
87 user_agent = models.CharField(required=False, max_length=512)
88 referrer = models.CharField(required=False, max_length=512)
89
90 created_at = models.DateTimeField(auto_now_add=True)
91
92 class Meta:
93 ordering = ["-created_at"]
94
95 @classmethod
96 def from_redirect(cls, redirect, request):
97 from_url = request.build_absolute_uri()
98 to_url = redirect.get_redirect_url(request)
99
100 if not to_url.startswith("http"):
101 to_url = request.build_absolute_uri(to_url)
102
103 if from_url == to_url:
104 raise ValueError("Redirecting to the same URL")
105
106 return cls.objects.create(
107 redirect=redirect,
108 from_url=from_url,
109 to_url=to_url,
110 http_status=redirect.http_status,
111 ip_address=_get_client_ip(request),
112 user_agent=request.headers.get("User-Agent", ""),
113 referrer=request.headers.get("Referer", ""),
114 )
115
116
117@models.register_model
118class NotFoundLog(models.Model):
119 url = models.URLField(max_length=512)
120
121 # Request metadata
122 ip_address = models.GenericIPAddressField()
123 user_agent = models.CharField(required=False, max_length=512)
124 referrer = models.CharField(required=False, max_length=512)
125
126 created_at = models.DateTimeField(auto_now_add=True)
127
128 class Meta:
129 ordering = ["-created_at"]
130
131 @classmethod
132 def from_request(cls, request):
133 return cls.objects.create(
134 url=request.build_absolute_uri(),
135 ip_address=_get_client_ip(request),
136 user_agent=request.headers.get("User-Agent", ""),
137 referrer=request.headers.get("Referer", ""),
138 )