From da25451f75da7ac4d2b1fd35e92e99dd30aeafee Mon Sep 17 00:00:00 2001
From: Immanuel Martini <immanuel.martini@gmail.com>
Date: Thu, 5 Sep 2024 20:09:20 +0200
Subject: [PATCH] Fix public url getter & update tests

---
 .../photoGallery/ProtectedMedia.test.tsx      | 19 ------
 .../photoGallery/ProtectedMedia.tsx           | 16 +----
 .../sidebar/SidebarDownloadMedia.tsx          | 15 +----
 ui/src/helpers/utils.test.ts                  | 60 +++++++++++++++----
 ui/src/helpers/utils.ts                       | 28 ++++++++-
 5 files changed, 78 insertions(+), 60 deletions(-)
 delete mode 100644 ui/src/components/photoGallery/ProtectedMedia.test.tsx

diff --git a/ui/src/components/photoGallery/ProtectedMedia.test.tsx b/ui/src/components/photoGallery/ProtectedMedia.test.tsx
deleted file mode 100644
index f8172f7..0000000
--- a/ui/src/components/photoGallery/ProtectedMedia.test.tsx
+++ /dev/null
@@ -1,19 +0,0 @@
-import { render, screen } from "@testing-library/react"
-import { ProtectedImage } from "./ProtectedMedia"
-
-const mockPublicUrl = 'http://localhost:9876';
-vi.mock('../../helpers/utils', () => ({getPublicUrl: () => mockPublicUrl}));
-
-describe('ProtectedImage', () => {
-    test.each([
-        {label: 'relative', src: '/image.jpg', expected: `${mockPublicUrl}/image.jpg`},
-        {label: 'absolute', src: 'http://localhost:4040/image.jpg', expected: 'http://localhost:4040/image.jpg'}
-    ])('loads image correctly given $label url', ({src, expected}) => {
-
-        render(<ProtectedImage alt={'alt_text'} src={src}/>);
-
-        const image = screen.getByAltText('alt_text');
-
-        expect(image).toHaveAttribute('src', expected);
-    })
-})
diff --git a/ui/src/components/photoGallery/ProtectedMedia.tsx b/ui/src/components/photoGallery/ProtectedMedia.tsx
index 0b3be42..822c2e3 100644
--- a/ui/src/components/photoGallery/ProtectedMedia.tsx
+++ b/ui/src/components/photoGallery/ProtectedMedia.tsx
@@ -4,26 +4,12 @@ import { useRef } from 'react'
 import { useState } from 'react'
 import { useEffect } from 'react'
 import { BlurhashCanvas } from 'react-blurhash'
-import { getPublicUrl, isNil } from '../../helpers/utils'
+import { getProtectedUrl, isNil } from '../../helpers/utils'
 
 const isNativeLazyLoadSupported = 'loading' in document.createElement('img')
 const placeholder =
   'data:image/gif;base64,R0lGODlhAQABAPAAAAAAAAAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw=='
 
-const getProtectedUrl = (url?: string) => {
-  if (url == undefined) return undefined
-
-  const imgUrl = new URL(url, getPublicUrl())
-
-  const tokenRegex = location.pathname.match(/^\/share\/([\d\w]+)(\/?.*)$/)
-  if (tokenRegex) {
-    const token = tokenRegex[1]
-    imgUrl.searchParams.set('token', token)
-  }
-
-  return imgUrl.href
-}
-
 export interface ProtectedImageProps
   extends Omit<
     DetailedHTMLProps<ImgHTMLAttributes<HTMLImageElement>, HTMLImageElement>,
diff --git a/ui/src/components/sidebar/SidebarDownloadMedia.tsx b/ui/src/components/sidebar/SidebarDownloadMedia.tsx
index ee3e44f..5149db2 100644
--- a/ui/src/components/sidebar/SidebarDownloadMedia.tsx
+++ b/ui/src/components/sidebar/SidebarDownloadMedia.tsx
@@ -1,7 +1,6 @@
 import { gql, useLazyQuery } from '@apollo/client'
 import { useTranslation } from 'react-i18next'
 import { NotificationType } from '../../__generated__/globalTypes'
-import { authToken } from '../../helpers/authentication'
 import { TranslationFn } from '../../localization'
 import { MessageState } from '../messages/Messages'
 import { MediaSidebarMedia } from './MediaSidebar/MediaSidebar'
@@ -13,7 +12,7 @@ import {
   sidebarDownloadQueryVariables,
   sidebarDownloadQuery_media_downloads,
 } from './__generated__/sidebarDownloadQuery'
-import { getPublicUrl } from '../../helpers/utils'
+import { getProtectedUrl } from '../../helpers/utils'
 
 export const SIDEBAR_DOWNLOAD_QUERY = gql`
   query sidebarDownloadQuery($mediaId: ID!) {
@@ -57,17 +56,9 @@ const formatBytes = (t: TranslationFn) => (bytes: number) => {
 }
 
 const downloadMedia = (t: TranslationFn) => async (url: string) => {
-  const imgUrl = new URL(url, getPublicUrl())
+  const imgUrl = getProtectedUrl(url);
 
-  if (authToken() == null) {
-    // Get share token if not authorized
-    const token = location.pathname.match(/^\/share\/([\d\w]+)(\/?.*)$/)
-    if (token) {
-      imgUrl.searchParams.set('token', token[1])
-    }
-  }
-
-  const response = await fetch(imgUrl.href, {
+  const response = await fetch(imgUrl, {
     credentials: 'include',
   })
 
diff --git a/ui/src/helpers/utils.test.ts b/ui/src/helpers/utils.test.ts
index b1ebcf2..d78fb4f 100644
--- a/ui/src/helpers/utils.test.ts
+++ b/ui/src/helpers/utils.test.ts
@@ -1,19 +1,55 @@
-import { getPublicUrl } from "./utils";
+import { getProtectedUrl, getPublicUrl } from "./utils";
 
-describe('getPublicUrl', () => {
+describe('Url helper', () => {
     const mockOrigin = 'http://localhost:9876';
-    Object.defineProperty(window, 'location', {
-        get() {
-            return { origin: mockOrigin };
-        }
+    const mockBaseUrl = '/base_url/';
+    const mockShareToken = 'nm42';
+    const mockSharePath = `/share/${mockShareToken}`;
+
+    beforeEach(()=>{        
+        import.meta.env.BASE_URL = mockBaseUrl;
+        Object.defineProperties(window,
+            {
+                location: {
+                    get() {
+                        return { origin: mockOrigin, pathname: mockSharePath };
+                    }
+                },
+                document: {
+                    get() {
+                        return { cookie: 'auth-token=abc' };
+                    }
+                }
+            }
+        );
     });
 
     test.each([
-        {label:'relative', baseUrl: '/my_public/', expected: `${mockOrigin}/my_public/`},
-        {label:'absolute', baseUrl: 'http://my_origin', expected: 'http://my_origin/'}
-    ])('returns currect url for $label base', ({baseUrl, expected}) => {
+        {url:'', baseUrl: '', expected: `${mockOrigin}/`},
+        {url:'', baseUrl: '/public_path/', expected: `${mockOrigin}/public_path/`},
+        {url:'/image.jpg', baseUrl: '/public_path/', expected: `${mockOrigin}/public_path/image.jpg`},
+        {url:'/image.jpg', baseUrl: 'http://other_host/', expected: 'http://other_host/image.jpg'},
+        {url:'http://other_host2/image.jpg', baseUrl: 'http://other_host/', expected: 'http://other_host2/image.jpg'}
+    ])('returns currect public url for base $baseUrl and url $url', ({url, baseUrl, expected}) => {
         import.meta.env.BASE_URL = baseUrl;
-        const url = getPublicUrl();
-        expect(url.href).toBe(expected);
+        const publicUrl = getPublicUrl(url);
+        expect(publicUrl.href).toBe(expected);
     })
-})
+
+    test('returns undefined protected url', () => {
+       expect(getProtectedUrl(undefined)).toBeUndefined();
+    })
+
+    test('returns protected url without token', () => {
+        expect(getProtectedUrl('image.jpg')).toBe(`${mockOrigin}${mockBaseUrl}image.jpg`);
+    })
+
+    test('returns protected url with token', () => {
+        Object.defineProperty(window, 'document', {
+            get() {
+                return { cookie: '' };
+            }
+        });
+        expect(getProtectedUrl('image.jpg')).toBe(`${mockOrigin}${mockBaseUrl}image.jpg?token=${mockShareToken}`);
+    })
+})
\ No newline at end of file
diff --git a/ui/src/helpers/utils.ts b/ui/src/helpers/utils.ts
index c37f02e..660a523 100644
--- a/ui/src/helpers/utils.ts
+++ b/ui/src/helpers/utils.ts
@@ -1,5 +1,6 @@
 import classNames, { Argument as ClassNamesArg } from 'classnames'
 import { overrideTailwindClasses } from 'tailwind-override'
+import { authToken } from './authentication'
 
 export interface DebouncedFn<F extends (...args: unknown[]) => unknown> {
   (...args: Parameters<F>): void
@@ -49,6 +50,29 @@ export function tailwindClassNames(...args: ClassNamesArg[]) {
   // return classNames(args)
 }
 
-export function getPublicUrl() {
-  return new URL(import.meta.env.BASE_URL, location.origin);
+export function getPublicUrl(url: string = '') {
+  try {
+    try {
+      return new URL(url);
+    } catch {
+      return new URL(url, import.meta.env.BASE_URL);
+    } 
+  } catch {
+      return new URL(`${import.meta.env.BASE_URL}${url}`.replace(/\/\//g, '/'), location.origin);
+  }
+}
+
+export function getProtectedUrl<S extends string | undefined>(url: S) {
+  if (url == undefined) return undefined as S
+
+  const publicUrl = getPublicUrl(url);
+
+  if (authToken() == null) {
+    const tokenRegex = location.pathname.match(/^\/share\/([\d\w]+)(\/?.*)$/)
+    if (tokenRegex) {
+      publicUrl.searchParams.set('token', tokenRegex[1])
+    }
+  }
+
+  return publicUrl.href
 }
