Plain is headed towards 1.0! Subscribe for development updates →

plain-redirection

  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        )