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