From 3381cd972d52d2abe096e641c3d1ccfaf471f14b Mon Sep 17 00:00:00 2001 From: sriram veeraghanta Date: Mon, 5 May 2025 17:45:48 +0530 Subject: [PATCH 1/4] chore: handling base path and urls --- apiserver/plane/authentication/utils/host.py | 13 ++++++++++--- apiserver/plane/settings/common.py | 17 +++++++++++------ apiserver/plane/utils/host.py | 12 +++++++++--- 3 files changed, 30 insertions(+), 12 deletions(-) diff --git a/apiserver/plane/authentication/utils/host.py b/apiserver/plane/authentication/utils/host.py index 64f19168592..67c8a4f723e 100644 --- a/apiserver/plane/authentication/utils/host.py +++ b/apiserver/plane/authentication/utils/host.py @@ -1,18 +1,25 @@ # Django imports from django.conf import settings from django.http import HttpRequest + # Third party imports from rest_framework.request import Request # Module imports from plane.utils.ip_address import get_client_ip -def base_host(request: Request | HttpRequest, is_admin: bool = False, is_space: bool = False, is_app: bool = False) -> str: + +def base_host( + request: Request | HttpRequest, + is_admin: bool = False, + is_space: bool = False, + is_app: bool = False, +) -> str: """Utility function to return host / origin from the request""" # Calculate the base origin from request base_origin = settings.WEB_URL or settings.APP_BASE_URL - # Admin redirections + # Admin redirection if is_admin: admin_base_path = getattr(settings, "ADMIN_BASE_PATH", "/god-mode/") if not admin_base_path.startswith("/"): @@ -25,7 +32,7 @@ def base_host(request: Request | HttpRequest, is_admin: bool = False, is_space: else: return base_origin + admin_base_path - # Space redirections + # Space redirection if is_space: space_base_path = getattr(settings, "SPACE_BASE_PATH", "/spaces/") if not space_base_path.startswith("/"): diff --git a/apiserver/plane/settings/common.py b/apiserver/plane/settings/common.py index 15d7a21b31f..6f6a98f0b44 100644 --- a/apiserver/plane/settings/common.py +++ b/apiserver/plane/settings/common.py @@ -312,13 +312,18 @@ # Base URLs ADMIN_BASE_URL = os.environ.get("ADMIN_BASE_URL", None) -ADMIN_BASE_PATH = os.environ.get("ADMIN_BASE_PATH", None) +ADMIN_BASE_PATH = os.environ.get("ADMIN_BASE_PATH", "/god-mode/") + SPACE_BASE_URL = os.environ.get("SPACE_BASE_URL", None) -SPACE_BASE_PATH = os.environ.get("SPACE_BASE_PATH", None) -APP_BASE_URL = os.environ.get("APP_BASE_URL") -APP_BASE_PATH = os.environ.get("APP_BASE_PATH", None) -LIVE_BASE_URL = os.environ.get("LIVE_BASE_URL") -LIVE_BASE_PATH = os.environ.get("LIVE_BASE_PATH") +SPACE_BASE_PATH = os.environ.get("SPACE_BASE_PATH", "/spaces/") + +APP_BASE_URL = os.environ.get("APP_BASE_URL", None) +APP_BASE_PATH = os.environ.get("APP_BASE_PATH", "/") + +LIVE_BASE_URL = os.environ.get("LIVE_BASE_URL", None) +LIVE_BASE_PATH = os.environ.get("LIVE_BASE_PATH", "/live/") +LIVE_URL = f"{LIVE_BASE_URL}{LIVE_BASE_PATH}" + WEB_URL = os.environ.get("WEB_URL") HARD_DELETE_AFTER_DAYS = int(os.environ.get("HARD_DELETE_AFTER_DAYS", 60)) diff --git a/apiserver/plane/utils/host.py b/apiserver/plane/utils/host.py index 7c8635836fd..d74a86ffdf2 100644 --- a/apiserver/plane/utils/host.py +++ b/apiserver/plane/utils/host.py @@ -9,7 +9,13 @@ # Module imports from plane.utils.ip_address import get_client_ip -def base_host(request: Request | HttpRequest, is_admin: bool = False, is_space: bool = False, is_app: bool = False) -> str: + +def base_host( + request: Request | HttpRequest, + is_admin: bool = False, + is_space: bool = False, + is_app: bool = False, +) -> str: """Utility function to return host / origin from the request""" # Calculate the base origin from request base_origin = settings.WEB_URL or settings.APP_BASE_URL @@ -17,7 +23,7 @@ def base_host(request: Request | HttpRequest, is_admin: bool = False, is_space: if not base_origin: raise ImproperlyConfigured("APP_BASE_URL or WEB_URL is not set") - # Admin redirections + # Admin redirection if is_admin: admin_base_path = getattr(settings, "ADMIN_BASE_PATH", "/god-mode/") if not admin_base_path.startswith("/"): @@ -30,7 +36,7 @@ def base_host(request: Request | HttpRequest, is_admin: bool = False, is_space: else: return base_origin + admin_base_path - # Space redirections + # Space redirection if is_space: space_base_path = getattr(settings, "SPACE_BASE_PATH", "/spaces/") if not space_base_path.startswith("/"): From 3d20e33696b620e415bfc76c9d5214a48e87169a Mon Sep 17 00:00:00 2001 From: pablohashescobar Date: Mon, 5 May 2025 18:29:04 +0530 Subject: [PATCH 2/4] chore: uniformize urls in common settings --- apiserver/plane/bgtasks/copy_s3_object.py | 8 ++++--- apiserver/plane/settings/common.py | 23 ++++++++++++++++++-- apiserver/plane/utils/url.py | 26 +++++++++++++++++++++++ 3 files changed, 52 insertions(+), 5 deletions(-) create mode 100644 apiserver/plane/utils/url.py diff --git a/apiserver/plane/bgtasks/copy_s3_object.py b/apiserver/plane/bgtasks/copy_s3_object.py index d73b96454d5..257f1d5dd6e 100644 --- a/apiserver/plane/bgtasks/copy_s3_object.py +++ b/apiserver/plane/bgtasks/copy_s3_object.py @@ -67,10 +67,12 @@ def sync_with_external_service(entity_name, description_html): "description_html": description_html, "variant": "rich" if entity_name == "PAGE" else "document", } + + if not settings.LIVE_URL: + return {} + response = requests.post( - f"{settings.LIVE_BASE_URL}/convert-document/", - json=data, - headers=None, + f"{settings.LIVE_URL}/convert-document/", json=data, headers=None ) if response.status_code == 200: return response.json() diff --git a/apiserver/plane/settings/common.py b/apiserver/plane/settings/common.py index 6f6a98f0b44..0d01bc5b5cd 100644 --- a/apiserver/plane/settings/common.py +++ b/apiserver/plane/settings/common.py @@ -13,6 +13,10 @@ from corsheaders.defaults import default_headers +# Module imports +from plane.utils.url import is_valid_url + + BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) # Secret Key @@ -310,20 +314,35 @@ CSRF_COOKIE_DOMAIN = os.environ.get("COOKIE_DOMAIN", None) CSRF_FAILURE_VIEW = "plane.authentication.views.common.csrf_failure" -# Base URLs +###### Base URLs ###### + +# Admin Base URL ADMIN_BASE_URL = os.environ.get("ADMIN_BASE_URL", None) +if ADMIN_BASE_URL and not is_valid_url(ADMIN_BASE_URL): + ADMIN_BASE_URL = None ADMIN_BASE_PATH = os.environ.get("ADMIN_BASE_PATH", "/god-mode/") +# Space Base URL SPACE_BASE_URL = os.environ.get("SPACE_BASE_URL", None) +if SPACE_BASE_URL and not is_valid_url(SPACE_BASE_URL): + SPACE_BASE_URL = None SPACE_BASE_PATH = os.environ.get("SPACE_BASE_PATH", "/spaces/") +# App Base URL APP_BASE_URL = os.environ.get("APP_BASE_URL", None) +if APP_BASE_URL and not is_valid_url(APP_BASE_URL): + APP_BASE_URL = None APP_BASE_PATH = os.environ.get("APP_BASE_PATH", "/") +# Live Base URL LIVE_BASE_URL = os.environ.get("LIVE_BASE_URL", None) +if LIVE_BASE_URL and not is_valid_url(LIVE_BASE_URL): + LIVE_BASE_URL = None LIVE_BASE_PATH = os.environ.get("LIVE_BASE_PATH", "/live/") -LIVE_URL = f"{LIVE_BASE_URL}{LIVE_BASE_PATH}" +LIVE_URL = f"{LIVE_BASE_URL}{LIVE_BASE_PATH}" if LIVE_BASE_URL else None + +# WEB URL WEB_URL = os.environ.get("WEB_URL") HARD_DELETE_AFTER_DAYS = int(os.environ.get("HARD_DELETE_AFTER_DAYS", 60)) diff --git a/apiserver/plane/utils/url.py b/apiserver/plane/utils/url.py new file mode 100644 index 00000000000..5feb08b95c2 --- /dev/null +++ b/apiserver/plane/utils/url.py @@ -0,0 +1,26 @@ +# Python imports +from urllib.parse import urlparse + + +def is_valid_url(url: str) -> bool: + """ + Validates whether the given string is a well-formed URL. + + Args: + url (str): The URL string to validate. + + Returns: + bool: True if the URL is valid, False otherwise. + + Example: + >>> is_valid_url("https://example.com") + True + >>> is_valid_url("not a url") + False + """ + try: + result = urlparse(url) + # A valid URL should have at least scheme and netloc + return all([result.scheme, result.netloc]) + except Exception: + return False From 7fa3ff9cb291ff20ce33b0eb690a899b4a543ecb Mon Sep 17 00:00:00 2001 From: pablohashescobar Date: Mon, 5 May 2025 18:50:07 +0530 Subject: [PATCH 3/4] correct live url --- apiserver/plane/bgtasks/copy_s3_object.py | 14 ++++++++--- apiserver/plane/utils/url.py | 30 ++++++++++++++++++++++- 2 files changed, 40 insertions(+), 4 deletions(-) diff --git a/apiserver/plane/bgtasks/copy_s3_object.py b/apiserver/plane/bgtasks/copy_s3_object.py index 257f1d5dd6e..97287339619 100644 --- a/apiserver/plane/bgtasks/copy_s3_object.py +++ b/apiserver/plane/bgtasks/copy_s3_object.py @@ -3,7 +3,7 @@ import base64 import requests from bs4 import BeautifulSoup - +from urllib.parse import urljoin # Django imports from django.conf import settings @@ -12,6 +12,7 @@ from plane.utils.exception_logger import log_exception from plane.settings.storage import S3Storage from celery import shared_task +from plane.utils.url import get_url_components def get_entity_id_field(entity_type, entity_id): @@ -71,9 +72,16 @@ def sync_with_external_service(entity_name, description_html): if not settings.LIVE_URL: return {} - response = requests.post( - f"{settings.LIVE_URL}/convert-document/", json=data, headers=None + live_url = get_url_components(settings.LIVE_URL) + if not live_url: + return {} + + base_url = ( + f"{live_url.get('scheme')}://{live_url.get('netloc')}{live_url.get('path')}" ) + url = urljoin(base_url, "convert-document/") + + response = requests.post(url, json=data, headers=None) if response.status_code == 200: return response.json() except requests.RequestException as e: diff --git a/apiserver/plane/utils/url.py b/apiserver/plane/utils/url.py index 5feb08b95c2..0658572bfe8 100644 --- a/apiserver/plane/utils/url.py +++ b/apiserver/plane/utils/url.py @@ -1,4 +1,5 @@ # Python imports +from typing import Optional from urllib.parse import urlparse @@ -22,5 +23,32 @@ def is_valid_url(url: str) -> bool: result = urlparse(url) # A valid URL should have at least scheme and netloc return all([result.scheme, result.netloc]) - except Exception: + except TypeError: return False + + +def get_url_components(url: str) -> Optional[dict]: + """ + Parses the URL and returns its components if valid. + + Args: + url (str): The URL string to parse. + + Returns: + Optional[dict]: A dictionary with URL components if valid, None otherwise. + + Example: + >>> get_url_components("https://example.com/path?query=1") + {'scheme': 'https', 'netloc': 'example.com', 'path': '/path', 'params': '', 'query': 'query=1', 'fragment': ''} + """ + if not is_valid_url(url): + return None + result = urlparse(url) + return { + "scheme": result.scheme, + "netloc": result.netloc, + "path": result.path, + "params": result.params, + "query": result.query, + "fragment": result.fragment, + } From d8eef4e1b40dacb9a1ef833c2708b9dec936216d Mon Sep 17 00:00:00 2001 From: pablohashescobar Date: Mon, 5 May 2025 18:55:21 +0530 Subject: [PATCH 4/4] chore: use url join to correctly join urls --- apiserver/plane/settings/common.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apiserver/plane/settings/common.py b/apiserver/plane/settings/common.py index 0d01bc5b5cd..38d2ac6e0ad 100644 --- a/apiserver/plane/settings/common.py +++ b/apiserver/plane/settings/common.py @@ -3,7 +3,7 @@ # Python imports import os from urllib.parse import urlparse - +from urllib.parse import urljoin # Third party imports import dj_database_url @@ -340,7 +340,7 @@ LIVE_BASE_URL = None LIVE_BASE_PATH = os.environ.get("LIVE_BASE_PATH", "/live/") -LIVE_URL = f"{LIVE_BASE_URL}{LIVE_BASE_PATH}" if LIVE_BASE_URL else None +LIVE_URL = urljoin(LIVE_BASE_URL, LIVE_BASE_PATH) if LIVE_BASE_URL else None # WEB URL WEB_URL = os.environ.get("WEB_URL")