Skip to content

Commit

Permalink
ref(media): upgrade getMediaDetails from v1 to v2 API endpoint (#1580)
Browse files Browse the repository at this point in the history
* ref(media): upgrade getMediaDetails from v1 to v2 API endpoint

* fix(media): adjust string array to Set
  • Loading branch information
dcshzj authored Oct 18, 2023
1 parent ad3d502 commit b563778
Show file tree
Hide file tree
Showing 10 changed files with 170 additions and 162 deletions.
15 changes: 1 addition & 14 deletions src/api.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
import axios from "axios"

import {
getNavFolderDropdownFromFolderOrder,
generateImageorFilePath,
} from "./utils/generate"
import { getNavFolderDropdownFromFolderOrder } from "./utils/generate"

// axios settings
axios.defaults.withCredentials = true
Expand Down Expand Up @@ -65,15 +62,6 @@ const getEditNavBarData = async (siteName) => {
}
}

const getMediaDetails = async ({ siteName, type, customPath, fileName }) => {
const resp = await axios.get(
`${process.env.REACT_APP_BACKEND_URL}/sites/${siteName}/${
type === "images" ? "images" : "documents"
}/${generateImageorFilePath(customPath, fileName)}`
)
return resp.data
}

// Login
const getEmailOtp = (email) => {
const endpoint = `${BACKEND_URL_V2}/user/email/otp`
Expand All @@ -100,7 +88,6 @@ const verifyMobileNumberOtp = async (mobile, otp) => {
export {
getEditNavBarData,
getAllCategories,
getMediaDetails,
getEmailOtp,
verifyEmailOtp,
getMobileNumberOtp,
Expand Down
1 change: 1 addition & 0 deletions src/constants/queryKeys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export const RESOURCE_ROOM_CONTENT_KEY = "resource-room-contents"
export const FOLDERS_CONTENT_KEY = "folders-contents"
export const LAST_UPDATED_KEY = "last-updated"
export const MEDIA_CONTENT_KEY = "media-contents"
export const MEDIA_MULTIPLE_CONTENT_KEY = "media-multiple-contents"
export const IMAGE_DETAILS_KEY = "image-details"
export const DOCUMENT_DETAILS_KEY = "document-details"
export const SETTINGS_CONTENT_KEY = "settings-content"
Expand Down
1 change: 1 addition & 0 deletions src/hooks/mediaHooks/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ export { useCreateMediaHook } from "./useCreateMediaHook"
export { useGetMediaHook } from "./useGetMediaHook"
export { useUpdateMediaHook } from "./useUpdateMediaHook"
export { useDeleteMediaHook } from "./useDeleteMediaHook"
export { useGetMultipleMediaHook } from "./useGetMultipleMediaHook"
50 changes: 50 additions & 0 deletions src/hooks/mediaHooks/useGetMultipleMediaHook.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { useQuery, UseQueryOptions, UseQueryResult } from "react-query"

import { MEDIA_MULTIPLE_CONTENT_KEY } from "constants/queryKeys"

import { apiService } from "services/ApiService"

import { MediaService } from "services"
import { MediaData } from "types/directory"
import { MultipleMediaParams } from "types/media"

const getMultipleMedia = (params: MultipleMediaParams) => {
const mediaService = new MediaService({ apiClient: apiService })

return Promise.all(
[...params.mediaSrcs].map((mediaSrc) => {
const mediaUrlTokens = mediaSrc.split("/")
// Note: The mediaUrl will always start with a "/"
const mediaDirectoryName = mediaUrlTokens.slice(1, -1).join("%2F")
const fileName = mediaUrlTokens.pop()

const reqParams = {
siteName: params.siteName,
mediaDirectoryName,
fileName,
}

return mediaService.get(reqParams).catch((error) => {
// It is possible for the user to specify a media that does not exist
// and we will automatically load the placeholder image for them
// The error needs to be caught here so that other images can still
// load, as Promise.all will fail if any of the promises fail
return error
}) as Promise<MediaData>
})
)
}

export const useGetMultipleMediaHook = (
params: MultipleMediaParams,
queryOptions?: Omit<UseQueryOptions<MediaData[]>, "queryFn" | "queryKey">
): UseQueryResult<MediaData[]> => {
return useQuery<MediaData[]>(
[MEDIA_MULTIPLE_CONTENT_KEY, params],
() => getMultipleMedia(params),
{
...queryOptions,
retry: false,
}
)
}
68 changes: 42 additions & 26 deletions src/layouts/EditPage.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import MarkdownEditor from "components/pages/MarkdownEditor"
import PagePreview from "components/pages/PagePreview"
import { WarningModal } from "components/WarningModal"

import { useGetMultipleMediaHook } from "hooks/mediaHooks"
import { useGetPageHook, useUpdatePageHook } from "hooks/pageHooks"
import { useCspHook, useGetSiteColorsHook } from "hooks/settingsHooks"
import useRedirectHook from "hooks/useRedirectHook"
Expand All @@ -33,7 +34,7 @@ import checkCSP from "utils/cspUtils"
import { isWriteActionsDisabled } from "utils/reviewRequests"
import { createPageStyleSheet } from "utils/siteColorUtils"

import { prependImageSrc } from "utils"
import { getMediaSrcsFromHtml, adjustImageSrcs } from "utils"

import "easymde/dist/easymde.min.css"

Expand Down Expand Up @@ -82,6 +83,7 @@ const EditPage = ({ match }) => {
const { siteName } = decodedParams

const [editorValue, setEditorValue] = useState("")
const [mediaSrcs, setMediaSrcs] = useState([])
const [currSha, setCurrSha] = useState("")
const [htmlChunk, setHtmlChunk] = useState("")

Expand Down Expand Up @@ -118,6 +120,38 @@ const EditPage = ({ match }) => {
const { data: csp } = useCspHook()
const { data: siteColorsData } = useGetSiteColorsHook(params)
const isWriteDisabled = isWriteActionsDisabled(siteName)
const { data: mediaData } = useGetMultipleMediaHook({
siteName,
mediaSrcs,
})

const updateMediaSrcs = () => {
if (!csp || _.isEmpty(csp) || !editorValue) return
const html = marked.parse(editorValue)
const { sanitisedHtml: CSPSanitisedHtml } = checkCSP(csp, html)
const DOMCSPSanitisedHtml = DOMPurify.sanitize(CSPSanitisedHtml)
setMediaSrcs(getMediaSrcsFromHtml(DOMCSPSanitisedHtml))
}

const updateHtmlWithMediaData = () => {
if (!csp || _.isEmpty(csp) || !editorValue) return
const html = marked.parse(editorValue)
const {
isCspViolation: checkedIsCspViolation,
sanitisedHtml: CSPSanitisedHtml,
} = checkCSP(csp, html)
const DOMCSPSanitisedHtml = DOMPurify.sanitize(CSPSanitisedHtml)
const processedChunk = adjustImageSrcs(DOMCSPSanitisedHtml, mediaData || [])

// Using FORCE_BODY adds a fake <remove></remove>
DOMPurify.removed = DOMPurify.removed.filter(
(el) => el.element?.tagName !== "REMOVE"
)

setIsXSSViolation(DOMPurify.removed.length > 0)
setIsContentViolation(checkedIsCspViolation)
setHtmlChunk(processedChunk)
}

/** ******************************** */
/* useEffects to load data */
Expand All @@ -130,45 +164,27 @@ const EditPage = ({ match }) => {
siteColorsData.primaryColor,
siteColorsData.secondaryColor
)
}, [siteColorsData])
}, [siteColorsData, siteName])

useEffect(() => {
if (pageData && !hasChanges) {
setEditorValue(pageData.content.pageBody.trim())
setCurrSha(pageData.sha)
}
}, [pageData])
}, [pageData, hasChanges])

useEffect(() => {
if (pageData)
setHasChanges(pageData.content.pageBody.trim() !== editorValue)
}, [pageData, editorValue])

useEffect(() => {
async function editorValueToHtml() {
if (!csp || _.isEmpty(csp) || !editorValue) return
const html = marked.parse(editorValue)
const {
isCspViolation: checkedIsCspViolation,
sanitisedHtml: CSPSanitisedHtml,
} = checkCSP(csp, html)
const DOMCSPSanitisedHtml = DOMPurify.sanitize(CSPSanitisedHtml)
const processedChunk = await prependImageSrc(
siteName,
DOMCSPSanitisedHtml
)

// Using FORCE_BODY adds a fake <remove></remove>
DOMPurify.removed = DOMPurify.removed.filter(
(el) => el.element?.tagName !== "REMOVE"
)
updateMediaSrcs()
}, [csp, editorValue])

setIsXSSViolation(DOMPurify.removed.length > 0)
setIsContentViolation(checkedIsCspViolation)
setHtmlChunk(processedChunk)
}
editorValueToHtml()
}, [csp, siteName, editorValue])
useEffect(() => {
updateHtmlWithMediaData()
}, [mediaData, editorValue])

return (
<VStack>
Expand Down
4 changes: 4 additions & 0 deletions src/types/media.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export interface MultipleMediaParams {
siteName: string
mediaSrcs: Set<string>
}
55 changes: 55 additions & 0 deletions src/utils/images.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
import * as cheerio from "cheerio"

import { MediaData } from "types/directory"

import { isLinkInternal } from "./misc"

/**
* Util method to retrieve image details from /images folder from the relative file path,
* e.g. "/images/album%/picture$.jpg" -> { imageDirectory: "images%2Falbum%25", fileName: "picture%24.jpg" }
Expand All @@ -18,3 +24,52 @@ export const getImageDetails = (
imageDirectory,
}
}

/**
* Util method to adjust image src in provided HTML, so that they can load from
* the correct source (i.e. GitHub or as base64).
*/
export const adjustImageSrcs = (
html: string,
mediaData: MediaData[]
): string => {
const $ = cheerio.load(html)

$("img").each((_, element) => {
const src = $(element).attr("src")

if (src && isLinkInternal(src)) {
$(element).attr(
"src",
mediaData.find((media) => src === `/${media.mediaPath}`)?.mediaUrl ||
"/placeholder_no_image.png"
)
}

// Set default placeholder image if image fails to load
$(element).attr(
"onerror",
"this.onerror=null; this.src='/placeholder_no_image.png';"
)
})

return $.html()
}

/**
* Util method to extract all image srcs that are internal from the provided HTML.
*/
export const getMediaSrcsFromHtml = (html: string): Set<string> => {
const $ = cheerio.load(html)
const mediaSrcs = new Set<string>()

$("img").each((_, element) => {
const src = $(element).attr("src")

if (src && isLinkInternal(src)) {
mediaSrcs.add(src)
}
})

return mediaSrcs
}
11 changes: 7 additions & 4 deletions src/utils/index.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
export * from "./generate"
export * from "./colours"
export * from "./date"
export * from "./decoding"
export * from "./deslugify"
export * from "./directoryUtils"
export * from "./fileNameUtils"
export * from "./files"
export * from "./generate"
export * from "./images"
export * from "./markdownToolbar"
export * from "./misc"
export * from "./siteColorUtils"
export * from "./text"
export * from "./toasts"
export * from "./validators"

export * from "./legacy"
export * from "./text"
export * from "./date"
export * from "./files"
Loading

0 comments on commit b563778

Please sign in to comment.