From a58affd6c63a6990cabe7aee0a9b6833345c1f26 Mon Sep 17 00:00:00 2001 From: Olivier Rousseau Date: Thu, 4 Jun 2026 11:57:14 -0400 Subject: [PATCH 1/3] chore: fixed typo newtwork > network --- .../Sources/InstructionsModal/Android/ConfigureAndroidApps.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/components/mode-specific/desktop/MySources/Sources/InstructionsModal/Android/ConfigureAndroidApps.js b/app/src/components/mode-specific/desktop/MySources/Sources/InstructionsModal/Android/ConfigureAndroidApps.js index d0eec416d7..d1f498ed87 100644 --- a/app/src/components/mode-specific/desktop/MySources/Sources/InstructionsModal/Android/ConfigureAndroidApps.js +++ b/app/src/components/mode-specific/desktop/MySources/Sources/InstructionsModal/Android/ConfigureAndroidApps.js @@ -8,7 +8,7 @@ const ConfigureAndroidApps = () => { From b4292db12d7bd03bbe520116a6f148e1591240fe Mon Sep 17 00:00:00 2001 From: rohanmathur91 Date: Tue, 30 Jun 2026 17:36:57 +0530 Subject: [PATCH 2/3] fix(profile): resolve avatar provider-first, else live Gravatar MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The header and Manage Account avatars rendered a one-time `photoURL` snapshot rather than resolving live. Google/SSO accounts stored the Google photo, while email/password accounts stored either a synthetic gravatar URL or the all-zeros dummy placeholder (avatar/0000…?d=mp&f=y). As a result, updates a user made on Gravatar never surfaced in the app. Add `getUserAvatarUrl(email, providerPhotoURL)`: - returns the provider photo (e.g. Google) unchanged when present - otherwise resolves Gravatar live from the current login email, so changes made on Gravatar are reflected — and the broken all-zeros snapshot is re-pointed to the real md5(email) hash Wire HeaderUser and the Manage Account profile page to it. parseGravatarImage is left intact (still used by OrgNotificationBanner). Co-Authored-By: Claude Opus 4.8 (1M context) --- .../components/Profile/ManageAccount.js | 5 ++--- .../DashboardLayout/MenuHeader/HeaderUser.jsx | 6 ++++-- app/src/utils/ImageUtils.js | 17 +++++++++++++++++ 3 files changed, 23 insertions(+), 5 deletions(-) diff --git a/app/src/features/settings/components/Profile/ManageAccount.js b/app/src/features/settings/components/Profile/ManageAccount.js index c6178f22ab..10c182eab8 100644 --- a/app/src/features/settings/components/Profile/ManageAccount.js +++ b/app/src/features/settings/components/Profile/ManageAccount.js @@ -5,6 +5,7 @@ import { Button as AntButton } from "antd"; import UserInfo from "./UserInfo"; //UTILS import { getUserAuthDetails } from "store/slices/global/user/selectors"; +import { getUserAvatarUrl } from "utils/ImageUtils"; import { redirectToDeleteAccount, redirectToSignDPA } from "../../../../utils/RedirectionUtils"; // ACTIONS import { handleForgotPasswordButtonOnClick } from "features/onboarding/components/auth/components/Form/actions"; @@ -21,9 +22,7 @@ const ManageAccount = () => { //Component State const [isChangePasswordLoading, setIsChangePasswordLoading] = useState(false); - // Fallback image - const defaultImageSrc = "https://www.gravatar.com/avatar/00000000000000000000000000000000?d=mp&f=y"; - let userImageSrc = user.details.profile.photoURL ? user.details.profile.photoURL : defaultImageSrc; + let userImageSrc = getUserAvatarUrl(user.details.profile.email, user.details.profile.photoURL); const userDisplayName = user.details.profile.displayName ? user.details.profile.displayName : "User"; diff --git a/app/src/layouts/DashboardLayout/MenuHeader/HeaderUser.jsx b/app/src/layouts/DashboardLayout/MenuHeader/HeaderUser.jsx index 3fd0225e55..aa8ca161e5 100644 --- a/app/src/layouts/DashboardLayout/MenuHeader/HeaderUser.jsx +++ b/app/src/layouts/DashboardLayout/MenuHeader/HeaderUser.jsx @@ -16,7 +16,7 @@ import { import { handleLogoutButtonOnClick } from "features/onboarding/components/auth/components/Form/actions"; import APP_CONSTANTS from "config/constants"; import { SOURCE } from "modules/analytics/events/common/constants"; -import { parseGravatarImage } from "utils/Misc"; +import { getUserAvatarUrl } from "utils/ImageUtils"; import { trackHeaderClicked } from "modules/analytics/events/common/onboarding/header"; import { trackUpgradeClicked } from "modules/analytics/events/misc/monetizationExperiment"; import { getAppFlavour } from "utils/AppUtils"; @@ -43,7 +43,9 @@ export default function HeaderUser() { const userName = user.loggedIn ? user?.details?.profile?.displayName ?? "User" : null; const userPhoto = - user.loggedIn && user?.details?.profile?.photoURL ? parseGravatarImage(user.details.profile.photoURL) : null; + user.loggedIn && user?.details?.profile + ? getUserAvatarUrl(user.details.profile.email, user.details.profile.photoURL) + : null; const userEmail = user?.details?.profile?.email; const planDetails = user?.details?.planDetails; diff --git a/app/src/utils/ImageUtils.js b/app/src/utils/ImageUtils.js index 2e2d011a51..25dace7f8d 100644 --- a/app/src/utils/ImageUtils.js +++ b/app/src/utils/ImageUtils.js @@ -14,3 +14,20 @@ export const getRandomAvatar = () => { export const generateGravatarURL = (email = "sagar@requestly.io") => { return `https://www.gravatar.com/avatar/${md5(email)}?s=200&d=${getRandomAvatar()}`; }; + +/** + * Resolves the avatar to display for a user. + * + * Precedence: the auth provider's photo (e.g. Google) when we have a real one, + * otherwise a Gravatar resolved live from the current login email — so changes a + * user makes on Gravatar are reflected here. For email/password accounts we persist + * a synthetic gravatar.com URL as `photoURL`, so any gravatar.com value is treated + * as "no provider photo" and re-resolved live from the email. `d=mp` is Gravatar's + * deterministic fallback, shown when the email has no Gravatar image. + */ +export const getUserAvatarUrl = (email = "", providerPhotoURL = "") => { + if (providerPhotoURL && !providerPhotoURL.includes("gravatar.com")) { + return providerPhotoURL; + } + return `https://www.gravatar.com/avatar/${md5(email)}?s=200&d=mp`; +}; From 37265ad47495cfeb5bf8fb7352f69e35cda34429 Mon Sep 17 00:00:00 2001 From: rohanmathur91 Date: Tue, 30 Jun 2026 18:30:12 +0530 Subject: [PATCH 3/3] fix(profile): match Gravatar by URL host, not substring (CodeQL) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CodeQL flagged providerPhotoURL.includes("gravatar.com") as incomplete URL substring sanitization — "gravatar.com" could appear in an unrelated host (gravatar.com.evil.com) or a query string. Parse the URL and match on the hostname instead (host === "gravatar.com" || host.endsWith(".gravatar.com")), falling back to live Gravatar on unparseable values. Co-Authored-By: Claude Opus 4.8 (1M context) --- app/src/utils/ImageUtils.js | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/app/src/utils/ImageUtils.js b/app/src/utils/ImageUtils.js index 25dace7f8d..ee0cab80ae 100644 --- a/app/src/utils/ImageUtils.js +++ b/app/src/utils/ImageUtils.js @@ -15,18 +15,33 @@ export const generateGravatarURL = (email = "sagar@requestly.io") => { return `https://www.gravatar.com/avatar/${md5(email)}?s=200&d=${getRandomAvatar()}`; }; +/** + * True when `url`'s host is Gravatar. Parses the URL and matches on the hostname + * rather than a substring — `url.includes("gravatar.com")` would also match + * unrelated hosts such as `gravatar.com.evil.com` or `host/path?ref=gravatar.com`. + * Unparseable values return false (treated as "not a Gravatar URL"). + */ +const isGravatarUrl = (url) => { + try { + const host = new URL(url).hostname.toLowerCase(); + return host === "gravatar.com" || host.endsWith(".gravatar.com"); + } catch { + return false; + } +}; + /** * Resolves the avatar to display for a user. * * Precedence: the auth provider's photo (e.g. Google) when we have a real one, * otherwise a Gravatar resolved live from the current login email — so changes a * user makes on Gravatar are reflected here. For email/password accounts we persist - * a synthetic gravatar.com URL as `photoURL`, so any gravatar.com value is treated + * a synthetic gravatar.com URL as `photoURL`, so any Gravatar-hosted value is treated * as "no provider photo" and re-resolved live from the email. `d=mp` is Gravatar's * deterministic fallback, shown when the email has no Gravatar image. */ export const getUserAvatarUrl = (email = "", providerPhotoURL = "") => { - if (providerPhotoURL && !providerPhotoURL.includes("gravatar.com")) { + if (providerPhotoURL && !isGravatarUrl(providerPhotoURL)) { return providerPhotoURL; } return `https://www.gravatar.com/avatar/${md5(email)}?s=200&d=mp`;