From efbccead12406abeea3de024063ec55774da3424 Mon Sep 17 00:00:00 2001 From: sriram veeraghanta Date: Sun, 25 May 2025 20:57:48 +0530 Subject: [PATCH 01/18] feat: added a python bg task to crawl work item links for title and description --- apiserver/plane/app/views/issue/link.py | 4 + .../plane/bgtasks/work_item_link_task.py | 158 ++++++++++++++++++ 2 files changed, 162 insertions(+) create mode 100644 apiserver/plane/bgtasks/work_item_link_task.py diff --git a/apiserver/plane/app/views/issue/link.py b/apiserver/plane/app/views/issue/link.py index d2641e0a4a1..243b08ad7ff 100644 --- a/apiserver/plane/app/views/issue/link.py +++ b/apiserver/plane/app/views/issue/link.py @@ -15,6 +15,7 @@ from plane.app.permissions import ProjectEntityPermission from plane.db.models import IssueLink from plane.bgtasks.issue_activities_task import issue_activity +from plane.bgtasks.work_item_link_task import crawl_work_item_link_title from plane.utils.host import base_host @@ -44,6 +45,9 @@ def create(self, request, slug, project_id, issue_id): serializer = IssueLinkSerializer(data=request.data) if serializer.is_valid(): serializer.save(project_id=project_id, issue_id=issue_id) + crawl_work_item_link_title.delay( + serializer.data.get("id"), serializer.data.get("url") + ) issue_activity.delay( type="link.activity.created", requested_data=json.dumps(serializer.data, cls=DjangoJSONEncoder), diff --git a/apiserver/plane/bgtasks/work_item_link_task.py b/apiserver/plane/bgtasks/work_item_link_task.py new file mode 100644 index 00000000000..b7599acffa4 --- /dev/null +++ b/apiserver/plane/bgtasks/work_item_link_task.py @@ -0,0 +1,158 @@ +# Third party imports +from celery import shared_task +import requests +from bs4 import BeautifulSoup +from urllib.parse import urlparse, urljoin +import base64 +import json + +from plane.db.models import IssueLink + + +DEFAULT_FAVICON = "PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGNsYXNzPSJsdWNpZGUgbHVjaWRlLWxpbmstaWNvbiBsdWNpZGUtbGluayI+PHBhdGggZD0iTTEwIDEzYTUgNSAwIDAgMCA3LjU0LjU0bDMtM2E1IDUgMCAwIDAtNy4wNy03LjA3bC0xLjcyIDEuNzEiLz48cGF0aCBkPSJNMTQgMTFhNSA1IDAgMCAwLTcuNTQtLjU0bC0zIDNhNSA1IDAgMCAwIDcuMDcgNy4wN2wxLjcxLTEuNzEiLz48L3N2Zz4=" # noqa: E501 + + +@shared_task +def crawl_work_item_link_title(id, url): + meta_data = crawl_work_item_link_title_and_favicon(url) + issue_link = IssueLink.objects.get(id=id) + issue_link.title = meta_data["title"] + issue_link.metadata = meta_data + issue_link.save() + + +def crawl_work_item_link_title_and_favicon(url): + """ + Crawls a URL to extract the title and favicon. + + Args: + url (str): The URL to crawl + + Returns: + str: JSON string containing title and base64-encoded favicon + """ + try: + # Set up headers to mimic a real browser + headers = { + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36" # noqa: E501 + } + + # Fetch the main page + response = requests.get(url, headers=headers, timeout=10) + response.raise_for_status() + + # Parse HTML + soup = BeautifulSoup(response.content, "html.parser") + + # Extract title + title_tag = soup.find("title") + title = title_tag.get_text().strip() if title_tag else "No title found" + + # Find favicon URL + favicon_url = find_favicon_url(soup, url) + + # Fetch and encode favicon + favicon_base64 = ( + fetch_and_encode_favicon(favicon_url, headers) + if favicon_url + else DEFAULT_FAVICON + ) + + # Prepare result + result = { + "title": title, + "favicon": favicon_base64, + "url": url, + "favicon_url": favicon_url, + } + + return json.dumps(result, indent=2) + + except requests.RequestException as e: + return json.dumps( + { + "error": f"Request failed: {str(e)}", + "title": None, + "favicon": None, + "url": url, + }, + indent=2, + ) + except Exception as e: + return json.dumps( + { + "error": f"Unexpected error: {str(e)}", + "title": None, + "favicon": None, + "url": url, + }, + indent=2, + ) + + +def find_favicon_url(soup, base_url): + """ + Find the favicon URL from HTML soup. + + Args: + soup: BeautifulSoup object + base_url: Base URL for resolving relative paths + + Returns: + str: Absolute URL to favicon or None + """ + # Look for various favicon link tags + favicon_selectors = [ + 'link[rel="icon"]', + 'link[rel="shortcut icon"]', + 'link[rel="apple-touch-icon"]', + 'link[rel="apple-touch-icon-precomposed"]', + ] + + for selector in favicon_selectors: + favicon_tag = soup.select_one(selector) + if favicon_tag and favicon_tag.get("href"): + return urljoin(base_url, favicon_tag["href"]) + + # Fallback to /favicon.ico + parsed_url = urlparse(base_url) + fallback_url = f"{parsed_url.scheme}://{parsed_url.netloc}/favicon.ico" + + # Check if fallback exists + try: + response = requests.head(fallback_url, timeout=5) + if response.status_code == 200: + return fallback_url + except Exception: + return DEFAULT_FAVICON + + return DEFAULT_FAVICON + + +def fetch_and_encode_favicon(favicon_url, headers): + """ + Fetch favicon and encode it as base64. + + Args: + favicon_url: URL to the favicon + headers: Request headers + + Returns: + str: Base64 encoded favicon with data URI prefix or None + """ + try: + response = requests.get(favicon_url, headers=headers, timeout=10) + response.raise_for_status() + + # Get content type + content_type = response.headers.get("content-type", "image/x-icon") + + # Convert to base64 + favicon_base64 = base64.b64encode(response.content).decode("utf-8") + + # Return as data URI + return f"data:{content_type};base64,{favicon_base64}" + + except Exception as e: + print(f"Failed to fetch favicon: {e}") + return None From 9d097d77f74863c885a8391a3be990e543cd35bc Mon Sep 17 00:00:00 2001 From: sangeethailango Date: Mon, 26 May 2025 15:44:44 +0530 Subject: [PATCH 02/18] fix: return meta_data in the response --- apiserver/plane/app/views/issue/link.py | 14 +++++++++++++- apiserver/plane/bgtasks/work_item_link_task.py | 3 ++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/apiserver/plane/app/views/issue/link.py b/apiserver/plane/app/views/issue/link.py index 243b08ad7ff..099550b8467 100644 --- a/apiserver/plane/app/views/issue/link.py +++ b/apiserver/plane/app/views/issue/link.py @@ -45,7 +45,7 @@ def create(self, request, slug, project_id, issue_id): serializer = IssueLinkSerializer(data=request.data) if serializer.is_valid(): serializer.save(project_id=project_id, issue_id=issue_id) - crawl_work_item_link_title.delay( + crawl_work_item_link_title( serializer.data.get("id"), serializer.data.get("url") ) issue_activity.delay( @@ -59,6 +59,10 @@ def create(self, request, slug, project_id, issue_id): notification=True, origin=base_host(request=request, is_app=True), ) + + issue_link = self.get_queryset().get(id=serializer.data.get("id")) + serializer = IssueLinkSerializer(issue_link) + return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) @@ -70,9 +74,14 @@ def partial_update(self, request, slug, project_id, issue_id, pk): current_instance = json.dumps( IssueLinkSerializer(issue_link).data, cls=DjangoJSONEncoder ) + serializer = IssueLinkSerializer(issue_link, data=request.data, partial=True) if serializer.is_valid(): serializer.save() + crawl_work_item_link_title( + serializer.data.get("id"), serializer.data.get("url") + ) + issue_activity.delay( type="link.activity.updated", requested_data=requested_data, @@ -84,6 +93,9 @@ def partial_update(self, request, slug, project_id, issue_id, pk): notification=True, origin=base_host(request=request, is_app=True), ) + issue_link = self.get_queryset().get(id=serializer.data.get("id")) + serializer = IssueLinkSerializer(issue_link) + return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) diff --git a/apiserver/plane/bgtasks/work_item_link_task.py b/apiserver/plane/bgtasks/work_item_link_task.py index b7599acffa4..0cfceccb844 100644 --- a/apiserver/plane/bgtasks/work_item_link_task.py +++ b/apiserver/plane/bgtasks/work_item_link_task.py @@ -16,8 +16,9 @@ def crawl_work_item_link_title(id, url): meta_data = crawl_work_item_link_title_and_favicon(url) issue_link = IssueLink.objects.get(id=id) - issue_link.title = meta_data["title"] + issue_link.metadata = meta_data + issue_link.save() From 4e6958f186bff0429c3a942556758738c424cf2d Mon Sep 17 00:00:00 2001 From: sangeethailango Date: Mon, 26 May 2025 16:01:09 +0530 Subject: [PATCH 03/18] fix: add validation for accessing IP ranges --- apiserver/plane/bgtasks/work_item_link_task.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/apiserver/plane/bgtasks/work_item_link_task.py b/apiserver/plane/bgtasks/work_item_link_task.py index 0cfceccb844..334e80be6fe 100644 --- a/apiserver/plane/bgtasks/work_item_link_task.py +++ b/apiserver/plane/bgtasks/work_item_link_task.py @@ -5,6 +5,7 @@ from urllib.parse import urlparse, urljoin import base64 import json +import ipaddress from plane.db.models import IssueLink @@ -33,6 +34,17 @@ def crawl_work_item_link_title_and_favicon(url): str: JSON string containing title and base64-encoded favicon """ try: + # Prevent access to private IP ranges + parsed = urlparse(url) + + try: + ip = ipaddress.ip_address(parsed.hostname) + if ip.is_private or ip.is_loopback or ip.is_reserved: + raise ValueError("Access to private/internal networks is not allowed") + except ValueError: + # Not an IP address, continue with domain validation + pass + # Set up headers to mimic a real browser headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36" # noqa: E501 From c75e92c79ca61374152cc1d653aeffd44f300402 Mon Sep 17 00:00:00 2001 From: sangeethailango Date: Mon, 26 May 2025 16:09:08 +0530 Subject: [PATCH 04/18] fix: remove json.dumps --- .../plane/bgtasks/work_item_link_task.py | 32 ++++++++----------- 1 file changed, 13 insertions(+), 19 deletions(-) diff --git a/apiserver/plane/bgtasks/work_item_link_task.py b/apiserver/plane/bgtasks/work_item_link_task.py index 334e80be6fe..75692046be5 100644 --- a/apiserver/plane/bgtasks/work_item_link_task.py +++ b/apiserver/plane/bgtasks/work_item_link_task.py @@ -79,28 +79,22 @@ def crawl_work_item_link_title_and_favicon(url): "favicon_url": favicon_url, } - return json.dumps(result, indent=2) + return result except requests.RequestException as e: - return json.dumps( - { - "error": f"Request failed: {str(e)}", - "title": None, - "favicon": None, - "url": url, - }, - indent=2, - ) + return { + "error": f"Request failed: {str(e)}", + "title": None, + "favicon": None, + "url": url, + } except Exception as e: - return json.dumps( - { - "error": f"Unexpected error: {str(e)}", - "title": None, - "favicon": None, - "url": url, - }, - indent=2, - ) + return { + "error": f"Unexpected error: {str(e)}", + "title": None, + "favicon": None, + "url": url, + } def find_favicon_url(soup, base_url): From cadaf86542b95737cca105ec2d0aa226e6a68ec2 Mon Sep 17 00:00:00 2001 From: sangeethailango Date: Mon, 26 May 2025 16:15:49 +0530 Subject: [PATCH 05/18] fix: handle exception by returning None --- apiserver/plane/bgtasks/work_item_link_task.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apiserver/plane/bgtasks/work_item_link_task.py b/apiserver/plane/bgtasks/work_item_link_task.py index 75692046be5..54356c48cf3 100644 --- a/apiserver/plane/bgtasks/work_item_link_task.py +++ b/apiserver/plane/bgtasks/work_item_link_task.py @@ -131,9 +131,9 @@ def find_favicon_url(soup, base_url): if response.status_code == 200: return fallback_url except Exception: - return DEFAULT_FAVICON + return None - return DEFAULT_FAVICON + return None def fetch_and_encode_favicon(favicon_url, headers): From 4ebbe000013cdced4da117e20835c47a912ce20c Mon Sep 17 00:00:00 2001 From: sangeethailango Date: Mon, 26 May 2025 16:37:41 +0530 Subject: [PATCH 06/18] refactor: call find_favicon_url inside fetch_and_encode_favicon function --- .../plane/bgtasks/work_item_link_task.py | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/apiserver/plane/bgtasks/work_item_link_task.py b/apiserver/plane/bgtasks/work_item_link_task.py index 54356c48cf3..cd2b8cf38e4 100644 --- a/apiserver/plane/bgtasks/work_item_link_task.py +++ b/apiserver/plane/bgtasks/work_item_link_task.py @@ -61,22 +61,15 @@ def crawl_work_item_link_title_and_favicon(url): title_tag = soup.find("title") title = title_tag.get_text().strip() if title_tag else "No title found" - # Find favicon URL - favicon_url = find_favicon_url(soup, url) - # Fetch and encode favicon - favicon_base64 = ( - fetch_and_encode_favicon(favicon_url, headers) - if favicon_url - else DEFAULT_FAVICON - ) + favicon_base64 = fetch_and_encode_favicon(headers, soup, url) # Prepare result result = { "title": title, - "favicon": favicon_base64, + "favicon": favicon_base64["favicon_base64"], "url": url, - "favicon_url": favicon_url, + "favicon_url": favicon_base64["favicon_url"], } return result @@ -136,7 +129,7 @@ def find_favicon_url(soup, base_url): return None -def fetch_and_encode_favicon(favicon_url, headers): +def fetch_and_encode_favicon(headers, soup, url): """ Fetch favicon and encode it as base64. @@ -148,6 +141,10 @@ def fetch_and_encode_favicon(favicon_url, headers): str: Base64 encoded favicon with data URI prefix or None """ try: + favicon_url = find_favicon_url(soup, url) + if favicon_url is None: + favicon_url = DEFAULT_FAVICON + response = requests.get(favicon_url, headers=headers, timeout=10) response.raise_for_status() @@ -158,7 +155,10 @@ def fetch_and_encode_favicon(favicon_url, headers): favicon_base64 = base64.b64encode(response.content).decode("utf-8") # Return as data URI - return f"data:{content_type};base64,{favicon_base64}" + return { + "favicon_url": favicon_url, + "favicon_base64": f"data:{content_type};base64,{favicon_base64}", + } except Exception as e: print(f"Failed to fetch favicon: {e}") From e7bbedf5d371fa440553006d47d5bf4190d52972 Mon Sep 17 00:00:00 2001 From: sangeethailango Date: Mon, 26 May 2025 16:56:55 +0530 Subject: [PATCH 07/18] chore: type hints --- apiserver/plane/bgtasks/work_item_link_task.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/apiserver/plane/bgtasks/work_item_link_task.py b/apiserver/plane/bgtasks/work_item_link_task.py index cd2b8cf38e4..8b4f4b1a7f9 100644 --- a/apiserver/plane/bgtasks/work_item_link_task.py +++ b/apiserver/plane/bgtasks/work_item_link_task.py @@ -4,8 +4,10 @@ from bs4 import BeautifulSoup from urllib.parse import urlparse, urljoin import base64 -import json import ipaddress +from typing import Dict, Any +from typing import Optional + from plane.db.models import IssueLink @@ -14,7 +16,7 @@ @shared_task -def crawl_work_item_link_title(id, url): +def crawl_work_item_link_title(id: str, url: str) -> None: meta_data = crawl_work_item_link_title_and_favicon(url) issue_link = IssueLink.objects.get(id=id) @@ -23,7 +25,7 @@ def crawl_work_item_link_title(id, url): issue_link.save() -def crawl_work_item_link_title_and_favicon(url): +def crawl_work_item_link_title_and_favicon(url: str) -> Dict[str, Any]: """ Crawls a URL to extract the title and favicon. @@ -90,7 +92,9 @@ def crawl_work_item_link_title_and_favicon(url): } -def find_favicon_url(soup, base_url): +def find_favicon_url(soup: BeautifulSoup, base_url: str) -> Optional[str]: + print(soup, "PRint soup") + print(base_url, "BaseURL") """ Find the favicon URL from HTML soup. @@ -129,7 +133,9 @@ def find_favicon_url(soup, base_url): return None -def fetch_and_encode_favicon(headers, soup, url): +def fetch_and_encode_favicon( + headers: Dict[str, str], soup: BeautifulSoup, url: str +) -> Optional[Dict[str, str]]: """ Fetch favicon and encode it as base64. From aea4c320f2114a5ad1a3621f2aeead07a1235aa8 Mon Sep 17 00:00:00 2001 From: sangeethailango Date: Mon, 26 May 2025 16:58:58 +0530 Subject: [PATCH 08/18] fix: Handle None --- apiserver/plane/bgtasks/work_item_link_task.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/apiserver/plane/bgtasks/work_item_link_task.py b/apiserver/plane/bgtasks/work_item_link_task.py index 8b4f4b1a7f9..3a70cdfe1dc 100644 --- a/apiserver/plane/bgtasks/work_item_link_task.py +++ b/apiserver/plane/bgtasks/work_item_link_task.py @@ -168,4 +168,7 @@ def fetch_and_encode_favicon( except Exception as e: print(f"Failed to fetch favicon: {e}") - return None + return { + "favicon_url": None, + "favicon_base64": f"data:image/svg+xml;base64,{DEFAULT_FAVICON}", + } From fa63779fb93a58607e6ed68557fa9a9d60b70661 Mon Sep 17 00:00:00 2001 From: sangeethailango Date: Mon, 26 May 2025 17:04:04 +0530 Subject: [PATCH 09/18] fix: remove print statementsg --- apiserver/plane/bgtasks/work_item_link_task.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/apiserver/plane/bgtasks/work_item_link_task.py b/apiserver/plane/bgtasks/work_item_link_task.py index 3a70cdfe1dc..3b3f51b353b 100644 --- a/apiserver/plane/bgtasks/work_item_link_task.py +++ b/apiserver/plane/bgtasks/work_item_link_task.py @@ -93,8 +93,6 @@ def crawl_work_item_link_title_and_favicon(url: str) -> Dict[str, Any]: def find_favicon_url(soup: BeautifulSoup, base_url: str) -> Optional[str]: - print(soup, "PRint soup") - print(base_url, "BaseURL") """ Find the favicon URL from HTML soup. From 83128c24a9b894779c70b099349297dfbe4276b3 Mon Sep 17 00:00:00 2001 From: JayashTripathy Date: Mon, 26 May 2025 20:31:35 +0530 Subject: [PATCH 10/18] chore: added favicon and title of links --- .../issues/issue-detail/links/link-item.tsx | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/web/core/components/issues/issue-detail/links/link-item.tsx b/web/core/components/issues/issue-detail/links/link-item.tsx index edb45bb3b8c..ebdba202d9c 100644 --- a/web/core/components/issues/issue-detail/links/link-item.tsx +++ b/web/core/components/issues/issue-detail/links/link-item.tsx @@ -38,6 +38,8 @@ export const IssueLinkItem: FC = observer((props) => { if (!linkDetail) return <>; const Icon = getIconForLink(linkDetail.url); + const faviconUrl: string | undefined = linkDetail.metadata?.favicon; + const linkTitle: string | undefined = linkDetail.metadata?.title; const toggleIssueLinkModal = (modalToggle: boolean) => { toggleIssueLinkModalStore(modalToggle); @@ -50,15 +52,21 @@ export const IssueLinkItem: FC = observer((props) => { className="group col-span-12 lg:col-span-6 xl:col-span-4 2xl:col-span-3 3xl:col-span-2 flex items-center justify-between gap-3 h-10 flex-shrink-0 px-3 bg-custom-background-90 hover:bg-custom-background-80 border-[0.5px] border-custom-border-200 rounded" > From 5d1a6b1e65219405bcf9e93dc885859c9c8f8db0 Mon Sep 17 00:00:00 2001 From: sangeethailango Date: Tue, 27 May 2025 17:13:23 +0530 Subject: [PATCH 11/18] fix: return null if no title found --- apiserver/plane/bgtasks/work_item_link_task.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apiserver/plane/bgtasks/work_item_link_task.py b/apiserver/plane/bgtasks/work_item_link_task.py index 3b3f51b353b..bfb163b9108 100644 --- a/apiserver/plane/bgtasks/work_item_link_task.py +++ b/apiserver/plane/bgtasks/work_item_link_task.py @@ -61,7 +61,7 @@ def crawl_work_item_link_title_and_favicon(url: str) -> Dict[str, Any]: # Extract title title_tag = soup.find("title") - title = title_tag.get_text().strip() if title_tag else "No title found" + title = title_tag.get_text().strip() if title_tag else None # Fetch and encode favicon favicon_base64 = fetch_and_encode_favicon(headers, soup, url) From bb5db0a51a812e04dff79885b7c2439ade4f5bb7 Mon Sep 17 00:00:00 2001 From: Sangeetha Date: Tue, 27 May 2025 17:58:35 +0530 Subject: [PATCH 12/18] Update apiserver/plane/bgtasks/work_item_link_task.py Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- .../plane/bgtasks/work_item_link_task.py | 21 +++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/apiserver/plane/bgtasks/work_item_link_task.py b/apiserver/plane/bgtasks/work_item_link_task.py index bfb163b9108..199bec991a8 100644 --- a/apiserver/plane/bgtasks/work_item_link_task.py +++ b/apiserver/plane/bgtasks/work_item_link_task.py @@ -147,8 +147,12 @@ def fetch_and_encode_favicon( try: favicon_url = find_favicon_url(soup, url) if favicon_url is None: - favicon_url = DEFAULT_FAVICON - + # Use default favicon directly + return { + "favicon_url": None, + "favicon_base64": f"data:image/svg+xml;base64,{DEFAULT_FAVICON}", + } + response = requests.get(favicon_url, headers=headers, timeout=10) response.raise_for_status() @@ -158,6 +162,19 @@ def fetch_and_encode_favicon( # Convert to base64 favicon_base64 = base64.b64encode(response.content).decode("utf-8") + # Return as data URI + return { + "favicon_url": favicon_url, + "favicon_base64": f"data:{content_type};base64,{favicon_base64}", + } + except Exception as e: + ... + # Get content type + content_type = response.headers.get("content-type", "image/x-icon") + + # Convert to base64 + favicon_base64 = base64.b64encode(response.content).decode("utf-8") + # Return as data URI return { "favicon_url": favicon_url, From 3e4d1a4847cb7526819c397b7eb9a53906b7bcde Mon Sep 17 00:00:00 2001 From: sangeethailango Date: Tue, 27 May 2025 18:07:20 +0530 Subject: [PATCH 13/18] fix: remove exception handling --- apiserver/plane/bgtasks/work_item_link_task.py | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/apiserver/plane/bgtasks/work_item_link_task.py b/apiserver/plane/bgtasks/work_item_link_task.py index 199bec991a8..5ea7a498c62 100644 --- a/apiserver/plane/bgtasks/work_item_link_task.py +++ b/apiserver/plane/bgtasks/work_item_link_task.py @@ -147,12 +147,11 @@ def fetch_and_encode_favicon( try: favicon_url = find_favicon_url(soup, url) if favicon_url is None: - # Use default favicon directly return { "favicon_url": None, "favicon_base64": f"data:image/svg+xml;base64,{DEFAULT_FAVICON}", } - + response = requests.get(favicon_url, headers=headers, timeout=10) response.raise_for_status() @@ -162,19 +161,6 @@ def fetch_and_encode_favicon( # Convert to base64 favicon_base64 = base64.b64encode(response.content).decode("utf-8") - # Return as data URI - return { - "favicon_url": favicon_url, - "favicon_base64": f"data:{content_type};base64,{favicon_base64}", - } - except Exception as e: - ... - # Get content type - content_type = response.headers.get("content-type", "image/x-icon") - - # Convert to base64 - favicon_base64 = base64.b64encode(response.content).decode("utf-8") - # Return as data URI return { "favicon_url": favicon_url, From 46808d567b2fd2549341dcca76cc7fb5ed55e08d Mon Sep 17 00:00:00 2001 From: sangeethailango Date: Wed, 28 May 2025 16:32:37 +0530 Subject: [PATCH 14/18] fix: reduce timeout seconds --- apiserver/plane/bgtasks/work_item_link_task.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apiserver/plane/bgtasks/work_item_link_task.py b/apiserver/plane/bgtasks/work_item_link_task.py index 5ea7a498c62..337ee475f81 100644 --- a/apiserver/plane/bgtasks/work_item_link_task.py +++ b/apiserver/plane/bgtasks/work_item_link_task.py @@ -53,7 +53,7 @@ def crawl_work_item_link_title_and_favicon(url: str) -> Dict[str, Any]: } # Fetch the main page - response = requests.get(url, headers=headers, timeout=10) + response = requests.get(url, headers=headers, timeout=2) response.raise_for_status() # Parse HTML From d87d2be6bbc96073473de2d3c0629a69fe3c4532 Mon Sep 17 00:00:00 2001 From: sangeethailango Date: Wed, 28 May 2025 18:58:51 +0530 Subject: [PATCH 15/18] fix: handle timeout exception --- .../plane/bgtasks/work_item_link_task.py | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/apiserver/plane/bgtasks/work_item_link_task.py b/apiserver/plane/bgtasks/work_item_link_task.py index 337ee475f81..fea5d1f669e 100644 --- a/apiserver/plane/bgtasks/work_item_link_task.py +++ b/apiserver/plane/bgtasks/work_item_link_task.py @@ -54,6 +54,14 @@ def crawl_work_item_link_title_and_favicon(url: str) -> Dict[str, Any]: # Fetch the main page response = requests.get(url, headers=headers, timeout=2) + + if requests.Timeout: + return { + "titile": None, + "favicon": f"data:image/svg+xml;base64,{DEFAULT_FAVICON}", + "url": url, + "error": f"Request Timeout", + } response.raise_for_status() # Parse HTML @@ -122,7 +130,7 @@ def find_favicon_url(soup: BeautifulSoup, base_url: str) -> Optional[str]: # Check if fallback exists try: - response = requests.head(fallback_url, timeout=5) + response = requests.head(fallback_url, timeout=2) if response.status_code == 200: return fallback_url except Exception: @@ -152,7 +160,14 @@ def fetch_and_encode_favicon( "favicon_base64": f"data:image/svg+xml;base64,{DEFAULT_FAVICON}", } - response = requests.get(favicon_url, headers=headers, timeout=10) + response = requests.get(favicon_url, headers=headers, timeout=2) + if requests.Timeout: + return { + "title": None, + "favicon": f"data:image/svg+xml;base64,{DEFAULT_FAVICON}", + "url": url, + "error": f"Request Timeout", + } response.raise_for_status() # Get content type From ed18c07a9adb1fe05f8633d57700bb62273a5484 Mon Sep 17 00:00:00 2001 From: sangeethailango Date: Wed, 28 May 2025 19:34:54 +0530 Subject: [PATCH 16/18] fix: remove request timeout handling --- apiserver/plane/bgtasks/work_item_link_task.py | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/apiserver/plane/bgtasks/work_item_link_task.py b/apiserver/plane/bgtasks/work_item_link_task.py index fea5d1f669e..c5c7a5d72b0 100644 --- a/apiserver/plane/bgtasks/work_item_link_task.py +++ b/apiserver/plane/bgtasks/work_item_link_task.py @@ -55,13 +55,6 @@ def crawl_work_item_link_title_and_favicon(url: str) -> Dict[str, Any]: # Fetch the main page response = requests.get(url, headers=headers, timeout=2) - if requests.Timeout: - return { - "titile": None, - "favicon": f"data:image/svg+xml;base64,{DEFAULT_FAVICON}", - "url": url, - "error": f"Request Timeout", - } response.raise_for_status() # Parse HTML @@ -161,13 +154,6 @@ def fetch_and_encode_favicon( } response = requests.get(favicon_url, headers=headers, timeout=2) - if requests.Timeout: - return { - "title": None, - "favicon": f"data:image/svg+xml;base64,{DEFAULT_FAVICON}", - "url": url, - "error": f"Request Timeout", - } response.raise_for_status() # Get content type From e3d8f32513f6119cafc633ec0c4742d952b33e2e Mon Sep 17 00:00:00 2001 From: JayashTripathy Date: Wed, 28 May 2025 20:03:00 +0530 Subject: [PATCH 17/18] feat: add Link icon to issue detail links and update rendering logic --- web/core/components/issues/issue-detail/links/link-item.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/web/core/components/issues/issue-detail/links/link-item.tsx b/web/core/components/issues/issue-detail/links/link-item.tsx index ebdba202d9c..83ddc4a7df9 100644 --- a/web/core/components/issues/issue-detail/links/link-item.tsx +++ b/web/core/components/issues/issue-detail/links/link-item.tsx @@ -2,7 +2,7 @@ import { FC } from "react"; import { observer } from "mobx-react"; -import { Pencil, Trash2, Copy } from "lucide-react"; +import { Pencil, Trash2, Copy, Link } from "lucide-react"; import { EIssueServiceType } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; import { TIssueServiceType } from "@plane/types"; @@ -37,7 +37,7 @@ export const IssueLinkItem: FC = observer((props) => { const linkDetail = getLinkById(linkId); if (!linkDetail) return <>; - const Icon = getIconForLink(linkDetail.url); + // const Icon = getIconForLink(linkDetail.url); const faviconUrl: string | undefined = linkDetail.metadata?.favicon; const linkTitle: string | undefined = linkDetail.metadata?.title; @@ -55,7 +55,7 @@ export const IssueLinkItem: FC = observer((props) => { {faviconUrl ? ( favicon ) : ( - + )} Date: Thu, 29 May 2025 15:22:17 +0530 Subject: [PATCH 18/18] fix: use logger for exception --- apiserver/plane/bgtasks/work_item_link_task.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/apiserver/plane/bgtasks/work_item_link_task.py b/apiserver/plane/bgtasks/work_item_link_task.py index c5c7a5d72b0..9a3ac265e1a 100644 --- a/apiserver/plane/bgtasks/work_item_link_task.py +++ b/apiserver/plane/bgtasks/work_item_link_task.py @@ -1,3 +1,7 @@ +# Python imports +import logging + + # Third party imports from celery import shared_task import requests @@ -7,9 +11,10 @@ import ipaddress from typing import Dict, Any from typing import Optional - - from plane.db.models import IssueLink +from plane.utils.exception_logger import log_exception + +logger = logging.getLogger("plane.worker") DEFAULT_FAVICON = "PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGNsYXNzPSJsdWNpZGUgbHVjaWRlLWxpbmstaWNvbiBsdWNpZGUtbGluayI+PHBhdGggZD0iTTEwIDEzYTUgNSAwIDAgMCA3LjU0LjU0bDMtM2E1IDUgMCAwIDAtNy4wNy03LjA3bC0xLjcyIDEuNzEiLz48cGF0aCBkPSJNMTQgMTFhNSA1IDAgMCAwLTcuNTQtLjU0bC0zIDNhNSA1IDAgMCAwIDcuMDcgNy4wN2wxLjcxLTEuNzEiLz48L3N2Zz4=" # noqa: E501 @@ -78,6 +83,7 @@ def crawl_work_item_link_title_and_favicon(url: str) -> Dict[str, Any]: return result except requests.RequestException as e: + log_exception(e) return { "error": f"Request failed: {str(e)}", "title": None, @@ -85,6 +91,7 @@ def crawl_work_item_link_title_and_favicon(url: str) -> Dict[str, Any]: "url": url, } except Exception as e: + log_exception(e) return { "error": f"Unexpected error: {str(e)}", "title": None, @@ -124,9 +131,11 @@ def find_favicon_url(soup: BeautifulSoup, base_url: str) -> Optional[str]: # Check if fallback exists try: response = requests.head(fallback_url, timeout=2) + response.raise_for_status() if response.status_code == 200: return fallback_url - except Exception: + except requests.RequestException as e: + log_exception(e) return None return None @@ -169,7 +178,7 @@ def fetch_and_encode_favicon( } except Exception as e: - print(f"Failed to fetch favicon: {e}") + logger.warning(f"Failed to fetch favicon: {e}") return { "favicon_url": None, "favicon_base64": f"data:image/svg+xml;base64,{DEFAULT_FAVICON}",