diff --git a/frappe/exceptions.py b/frappe/exceptions.py index f4bcb661f1..2258c6e5ae 100644 --- a/frappe/exceptions.py +++ b/frappe/exceptions.py @@ -64,7 +64,8 @@ class RequestToken(Exception): class Redirect(Exception): - http_status_code = 301 + def __init__(self, http_status_code: int = 301): + self.http_status_code = http_status_code class CSRFTokenError(Exception): diff --git a/frappe/tests/test_website.py b/frappe/tests/test_website.py index b7b33b3531..2ab4ee8a0e 100644 --- a/frappe/tests/test_website.py +++ b/frappe/tests/test_website.py @@ -170,10 +170,18 @@ class TestWebsite(FrappeTestCase): dict( source=r"/courses/course\?course=(.*)", target=r"/courses/\1", match_with_query_string=True ), + dict( + source="/test307", + target="/test", + redirect_http_status=307, + ), ] website_settings = frappe.get_doc("Website Settings") - website_settings.append("route_redirects", {"source": "/testsource", "target": "/testtarget"}) + website_settings.append( + "route_redirects", + {"source": "/testsource", "target": "/testtarget", "redirect_http_status": 301}, + ) website_settings.save() set_request(method="GET", path="/testfrom") @@ -205,6 +213,16 @@ class TestWebsite(FrappeTestCase): self.assertEqual(response.status_code, 301) self.assertEqual(response.headers.get("Location"), "/courses/data") + set_request(method="GET", path="/test307") + response = get_response() + self.assertEqual(response.status_code, 307) + self.assertEqual(response.headers.get("Location"), "/test") + + set_request(method="POST", path="/test307") + response = get_response() + self.assertEqual(response.status_code, 307) + self.assertEqual(response.headers.get("Location"), "/test") + delattr(frappe.hooks, "website_redirects") frappe.cache.delete_key("app_hooks") diff --git a/frappe/website/doctype/website_route_redirect/website_route_redirect.json b/frappe/website/doctype/website_route_redirect/website_route_redirect.json index d561a1be10..30ace0b8ba 100644 --- a/frappe/website/doctype/website_route_redirect/website_route_redirect.json +++ b/frappe/website/doctype/website_route_redirect/website_route_redirect.json @@ -1,10 +1,12 @@ { + "actions": [], "creation": "2019-05-07 11:08:35.889625", "doctype": "DocType", "engine": "InnoDB", "field_order": [ "source", - "target" + "target", + "redirect_http_status" ], "fields": [ { @@ -20,10 +22,19 @@ "in_list_view": 1, "label": "Target", "reqd": 1 + }, + { + "default": "301", + "fieldname": "redirect_http_status", + "fieldtype": "Int", + "label": "Redirect HTTP Status", + "options": "301\n302\n307\n308", + "reqd": 1 } ], "istable": 1, - "modified": "2019-05-07 11:11:46.867684", + "links": [], + "modified": "2023-12-13 12:09:50.726082", "modified_by": "Administrator", "module": "Website", "name": "Website Route Redirect", @@ -31,5 +42,6 @@ "permissions": [], "quick_entry": 1, "sort_field": "modified", - "sort_order": "ASC" + "sort_order": "ASC", + "states": [] } \ No newline at end of file diff --git a/frappe/website/doctype/website_route_redirect/website_route_redirect.py b/frappe/website/doctype/website_route_redirect/website_route_redirect.py index 32949fb229..616c8f28ac 100644 --- a/frappe/website/doctype/website_route_redirect/website_route_redirect.py +++ b/frappe/website/doctype/website_route_redirect/website_route_redirect.py @@ -17,7 +17,9 @@ class WebsiteRouteRedirect(Document): parent: DF.Data parentfield: DF.Data parenttype: DF.Data + redirect_http_status: DF.Int source: DF.SmallText target: DF.SmallText # end: auto-generated types + pass diff --git a/frappe/website/page_renderers/redirect_page.py b/frappe/website/page_renderers/redirect_page.py index be4a7bdf45..cfb9a085d8 100644 --- a/frappe/website/page_renderers/redirect_page.py +++ b/frappe/website/page_renderers/redirect_page.py @@ -14,7 +14,7 @@ class RedirectPage: return build_response( self.path, "", - 301, + self.http_status_code, { "Location": frappe.flags.redirect_location or (frappe.local.response or {}).get("location"), "Cache-Control": "no-store, no-cache, must-revalidate", diff --git a/frappe/website/path_resolver.py b/frappe/website/path_resolver.py index 811977b451..0ff1a8f0e3 100644 --- a/frappe/website/path_resolver.py +++ b/frappe/website/path_resolver.py @@ -35,8 +35,8 @@ class PathResolver: try: resolve_redirect(self.path, request.query_string) - except frappe.Redirect: - return frappe.flags.redirect_location, RedirectPage(self.path) + except frappe.Redirect as e: + return frappe.flags.redirect_location, RedirectPage(self.path, e.http_status_code) endpoint = resolve_path(self.path) @@ -105,7 +105,9 @@ def resolve_redirect(path, query_string=None): ] """ redirects = frappe.get_hooks("website_redirects") - redirects += frappe.get_all("Website Route Redirect", ["source", "target"], order_by=None) + redirects += frappe.get_all( + "Website Route Redirect", ["source", "target", "redirect_http_status"], order_by=None + ) if not redirects: return @@ -113,6 +115,9 @@ def resolve_redirect(path, query_string=None): redirect_to = frappe.cache.hget("website_redirects", path) if redirect_to: + if isinstance(redirect_to, dict): + frappe.flags.redirect_location = redirect_to["path"] + raise frappe.Redirect(redirect_to["status_code"]) frappe.flags.redirect_location = redirect_to raise frappe.Redirect @@ -124,14 +129,17 @@ def resolve_redirect(path, query_string=None): try: match = re.match(pattern, path_to_match) - except re.error as e: + except re.error: frappe.log_error("Broken Redirect: " + pattern) if match: redirect_to = re.sub(pattern, rule["target"], path_to_match) frappe.flags.redirect_location = redirect_to - frappe.cache.hset("website_redirects", path_to_match, redirect_to) - raise frappe.Redirect + status_code = rule.get("redirect_http_status", 301) + frappe.cache.hset( + "website_redirects", path_to_match, {"path": redirect_to, "status_code": status_code} + ) + raise frappe.Redirect(status_code) def resolve_path(path): diff --git a/frappe/website/serve.py b/frappe/website/serve.py index acae44940e..2261683406 100644 --- a/frappe/website/serve.py +++ b/frappe/website/serve.py @@ -16,8 +16,8 @@ def get_response(path=None, http_status_code=200): path_resolver = PathResolver(path, http_status_code) endpoint, renderer_instance = path_resolver.resolve() response = renderer_instance.render() - except frappe.Redirect: - return RedirectPage(endpoint or path, http_status_code).render() + except frappe.Redirect as e: + return RedirectPage(endpoint or path, e.http_status_code).render() except frappe.PermissionError as e: response = NotPermittedPage(endpoint, http_status_code, exception=e).render() except frappe.PageDoesNotExistError: