Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 1 addition & 18 deletions fxsharing/shares/tasks.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import functools
import mimetypes
from urllib.parse import urljoin, urlparse

from django.conf import settings
Expand All @@ -18,16 +17,6 @@
logger = get_task_logger(__name__)

FAVICON_MAX_BYTES = 1 * 1024 * 1024 # 1 MB cap
CONTENT_TYPE_TO_EXT = {
"image/png": ".png",
"image/jpeg": ".jpg",
"image/jpg": ".jpg",
"image/gif": ".gif",
"image/webp": ".webp",
"image/svg+xml": ".svg",
"image/x-icon": ".ico",
"image/vnd.microsoft.icon": ".ico",
}


@functools.cache
Expand Down Expand Up @@ -76,14 +65,8 @@ def download_and_store_favicon(favicon_url, link_url, headers):
)
return None

ext = (
CONTENT_TYPE_TO_EXT.get(content_type)
or mimetypes.guess_extension(content_type)
or ".ico"
)

hostname = urlparse(link_url).hostname
object_name = f"favicons/{hostname}{ext}"
object_name = f"favicons/{hostname}"

client = _get_gcs_client()
bucket = client.bucket(bucket_name)
Expand Down
1 change: 1 addition & 0 deletions fxsharing/shares/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
path("event", views.record_client_event, name="record_client_event"),
path("report/<str:shortcode>", views.report_share, name="report_share"),
path("auth-complete", views.auth_complete, name="auth_complete"),
path("get_favicon_url", views.get_favicon_url, name="get_favicon_url"),
]

if settings.DEBUG:
Expand Down
33 changes: 32 additions & 1 deletion fxsharing/shares/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import json
import logging
from datetime import timedelta
from urllib.parse import urlparse

from django.conf import settings
from django.contrib import messages
Expand Down Expand Up @@ -36,7 +37,12 @@
from .cinder_schema import decision_created_schema
from .models import Link, Share, ShareStatus
from .share_schema import share_schema
from .tasks import process_new_share, purge_cdn_cache, submit_share_to_cinder
from .tasks import (
_get_gcs_client,
process_new_share,
purge_cdn_cache,
submit_share_to_cinder,
)

log = logging.getLogger(__name__)

Expand Down Expand Up @@ -96,6 +102,31 @@ def view_share(request, shortcode):
return response


def get_favicon_url(request):
bucket_name = settings.GCS_IMAGE_BUCKET
if not bucket_name:
return None

favicons = {}

urls = set([urlparse(url).hostname for url in request.GET.getlist("url")])

for hostname in urls:
object_name = f"favicons/{hostname}"

client = _get_gcs_client()
bucket = client.bucket(bucket_name)
blob = bucket.blob(object_name)

favicon_url = None
if blob.exists():
favicon_url = f"https://storage.googleapis.com/{bucket_name}/{object_name}"
favicons[hostname] = favicon_url

# returns a dict with hostname as key and the favicon url as the value
return JsonResponse(favicons)


SHARE_EXPIRY_DAYS = 7


Expand Down
57 changes: 57 additions & 0 deletions static/components/moz-share.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -429,8 +429,13 @@ class MozShare extends MozLitElement {
static queries = {
reportDialog: "#report-dialog",
reportForm: "#report-dialog form",
mozLinks: { all: "moz-link" },
};

static MAX_RETRIES = 10;
static BASE_DELAY = 200;
static MAX_DELAY = 20000;

connectedCallback() {
super.connectedCallback();
this.init();
Expand Down Expand Up @@ -462,6 +467,8 @@ class MozShare extends MozLitElement {
} catch (e) {
console.error(e);
}

this.setupPollingForFavicons();
}

/**
Expand All @@ -484,6 +491,56 @@ class MozShare extends MozLitElement {
return links;
}

get linksWithoutFavicons() {
return this.flatLinks.filter((l) => !l.favicon_url);
}

async setupPollingForFavicons() {
const shouldPollForFavicon = this.linksWithoutFavicons.length > 0;

if (shouldPollForFavicon) {
let attempt = 0;

while (attempt < MozShare.MAX_RETRIES) {
try {
return await this.pollForFavicon();
} catch {
// Use exponential backoff for requesting the favicons
const delay = Math.min(
MozShare.MAX_DELAY,
MozLink.BASE_DELAY * Math.pow(2, attempt),
);
const jitter = Math.random() * delay;

await new Promise((resolve) => setTimeout(resolve, delay + jitter));

attempt += 1;
}
}
}
}

async pollForFavicon() {
let params = new URLSearchParams();
this.linksWithoutFavicons.forEach((l) => params.append("url", l.url));

let response = await fetch(`/get_favicon_url?${params.toString()}`);

let favicons = await response.json();
if (favicons) {
for (let [hostname, favicon_url] of Object.entries(favicons)) {
let matchedHosts = this.linksWithoutFavicons.filter(
(l) => new URL(l.url).hostname === hostname,
);
matchedHosts.forEach((l) => (l.favicon_url = favicon_url));
}
this.mozLinks.forEach((ml) => ml.requestUpdate());
return;
}

throw new Error("Failed to get favicon on this attempt");
}

copyLink() {
navigator.clipboard.writeText(location.href);
recordEvent("copy_link", {});
Expand Down