Plain is headed towards 1.0! Subscribe for development updates →

  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        )