Merge remote-tracking branch 'origin/master' into add-livephoto-support

This commit is contained in:
Rushikesh Tote 2022-02-26 20:16:32 +05:30
commit 4e33ce0cf6
No known key found for this signature in database
GPG key ID: E4461ACF821B1DA8
18 changed files with 281 additions and 46 deletions

View file

@ -28,6 +28,8 @@ module.exports = {
'base-uri ': "'self'",
'frame-ancestors': " 'none'",
'form-action': "'none'",
'report-uri': ' https://csp-reporter.ente.io',
'report-to': ' https://csp-reporter.ente.io',
},
WORKBOX_CONFIG: {
@ -38,10 +40,9 @@ module.exports = {
ALL_ROUTES: '/(.*)',
buildCSPHeader: (directives) => ({
'Content-Security-Policy': Object.entries(directives).reduce(
(acc, [key, value]) => acc + `${key} ${value};`,
''
),
'Content-Security-Policy-Report-Only': Object.entries(
directives
).reduce((acc, [key, value]) => acc + `${key} ${value};`, ''),
}),
convertToNextHeaderFormat: (headers) =>

View file

@ -1,6 +1,6 @@
{
"name": "bada-frame",
"version": "0.6.1",
"version": "0.7.0",
"private": true,
"scripts": {
"dev": "next dev",

View file

@ -8,5 +8,5 @@
X-Frame-Options: deny
X-XSS-Protection: 1; mode=block
Referrer-Policy: same-origin
Content-Security-Policy-Report-Only: default-src 'none'; img-src 'self' blob:; media-src 'self' blob:; style-src 'self' 'unsafe-inline'; font-src 'self'; script-src 'self' 'unsafe-eval' blob:; manifest-src': 'self'; connect-src 'self' https://*.ente.io data: blob: https://ente-prod-eu.s3.eu-central-003.backblazeb2.com ; base-uri 'self'; frame-ancestors 'none'; form-action 'none'; report-uri https://csp-reporter.ente.io; report-to https://csp-reporter.ente.io;
Content-Security-Policy-Report-Only: default-src 'none'; img-src 'self' blob:; media-src 'self' blob:; style-src 'self' 'unsafe-inline'; font-src 'self'; script-src 'self' 'unsafe-eval' blob:; manifest-src 'self'; connect-src 'self' https://*.ente.io data: blob: https://ente-prod-eu.s3.eu-central-003.backblazeb2.com ; base-uri 'self'; frame-ancestors 'none'; form-action 'none'; report-uri https://csp-reporter.ente.io; report-to https://csp-reporter.ente.io;

View file

@ -29,6 +29,11 @@ Sentry.init({
event.request.url = currentURL;
return event;
},
integrations: function (i) {
return i.filter(function (i) {
return i.name !== 'Breadcrumbs';
});
},
// ...
// Note: if you want to override the automatic release value, do not set a
// `release` value here - use the environment variable `SENTRY_RELEASE`, so

View file

@ -69,6 +69,7 @@ interface Props {
deleted?: number[];
activeCollection: number;
isSharedCollection: boolean;
enableDownload: boolean;
}
const PhotoFrame = ({
@ -86,6 +87,7 @@ const PhotoFrame = ({
deleted,
activeCollection,
isSharedCollection,
enableDownload,
}: Props) => {
const [open, setOpen] = useState(false);
const [currentIndex, setCurrentIndex] = useState<number>(0);
@ -464,7 +466,8 @@ const PhotoFrame = ({
url =
await PublicCollectionDownloadManager.getThumbnail(
item,
publicCollectionGalleryContext.token
publicCollectionGalleryContext.token,
publicCollectionGalleryContext.passwordToken
);
} else {
url = await DownloadManager.getThumbnail(item);
@ -501,6 +504,7 @@ const PhotoFrame = ({
url = await PublicCollectionDownloadManager.getFile(
item,
publicCollectionGalleryContext.token,
publicCollectionGalleryContext.passwordToken,
true
);
} else {
@ -578,6 +582,7 @@ const PhotoFrame = ({
favItemIds={favItemIds}
isSharedCollection={isSharedCollection}
isTrashCollection={activeCollection === TRASH_SECTION}
enableDownload={enableDownload}
/>
</Container>
)}

View file

@ -71,6 +71,7 @@ interface Iprops {
favItemIds: Set<number>;
isSharedCollection: boolean;
isTrashCollection: boolean;
enableDownload: boolean;
}
const LegendContainer = styled.div`
@ -704,7 +705,8 @@ function PhotoSwipe(props: Iprops) {
await downloadFile(
file,
publicCollectionGalleryContext.accessedThroughSharedURL,
publicCollectionGalleryContext.token
publicCollectionGalleryContext.token,
publicCollectionGalleryContext.passwordToken
);
galleryContext.finishLoading();
@ -738,14 +740,15 @@ function PhotoSwipe(props: Iprops) {
title={constants.CLOSE}
/>
<button
className="pswp-custom download-btn"
title={constants.DOWNLOAD}
onClick={() =>
downloadFileHelper(photoSwipe.currItem)
}
/>
{props.enableDownload && (
<button
className="pswp-custom download-btn"
title={constants.DOWNLOAD}
onClick={() =>
downloadFileHelper(photoSwipe.currItem)
}
/>
)}
<button
className="pswp__button pswp__button--fs"
title={constants.TOGGLE_FULLSCREEN}

View file

@ -194,7 +194,8 @@ export default function PreviewCard(props: IProps) {
url =
await PublicCollectionDownloadManager.getThumbnail(
file,
publicCollectionGalleryContext.token
publicCollectionGalleryContext.token,
publicCollectionGalleryContext.passwordToken
);
} else {
url = await DownloadManager.getThumbnail(file);

View file

@ -67,6 +67,7 @@ export default function Credentials() {
keyAttributes.keyDecryptionNonce,
kek
);
if (isFirstLogin()) {
await generateAndSaveIntermediateKeyAttributes(
passphrase,

View file

@ -692,6 +692,7 @@ export default function Gallery() {
activeCollection,
collections
)}
enableDownload={true}
/>
{selected.count > 0 &&
selected.collectionID === activeCollection && (

View file

@ -3,11 +3,15 @@ import PhotoFrame from 'components/PhotoFrame';
import React, { useContext, useEffect, useRef, useState } from 'react';
import {
getLocalPublicCollection,
getLocalPublicCollectionPassword,
getLocalPublicFiles,
getPublicCollection,
getPublicCollectionUID,
removePublicCollectionWithFiles,
removePublicFiles,
savePublicCollectionPassword,
syncPublicFiles,
verifyPublicCollectionPassword,
} from 'services/publicCollectionService';
import { Collection } from 'types/collection';
import { EnteFile } from 'types/file';
@ -28,6 +32,9 @@ import LoadingBar from 'react-top-loading-bar';
import CryptoWorker from 'utils/crypto';
import { PAGES } from 'constants/pages';
import { useRouter } from 'next/router';
import SingleInputForm from 'components/SingleInputForm';
import { Card } from 'react-bootstrap';
import { logError } from 'utils/sentry';
const Loader = () => (
<Container>
@ -39,6 +46,8 @@ const Loader = () => (
const bs58 = require('bs58');
export default function PublicCollectionGallery() {
const token = useRef<string>(null);
// passwordJWTToken refers to the jwt token which is used for album protected by password.
const passwordJWTToken = useRef<string>(null);
const collectionKey = useRef<string>(null);
const url = useRef<string>(null);
const [publicFiles, setPublicFiles] = useState<EnteFile[]>(null);
@ -54,6 +63,8 @@ export default function PublicCollectionGallery() {
const loadingBar = useRef(null);
const isLoadingBarRunning = useRef(false);
const router = useRouter();
const [isPasswordProtected, setIsPasswordProtected] =
useState<boolean>(false);
const openMessageDialog = () => setMessageDialogView(true);
const closeMessageDialog = () => setMessageDialogView(false);
@ -115,6 +126,8 @@ export default function PublicCollectionGallery() {
mergeMetadata(localFiles)
);
setPublicFiles(localPublicFiles);
passwordJWTToken.current =
await getLocalPublicCollectionPassword(collectionUID);
}
await syncWithRemote();
} finally {
@ -127,6 +140,7 @@ export default function PublicCollectionGallery() {
useEffect(openMessageDialog, [dialogMessage]);
const syncWithRemote = async () => {
const collectionUID = getPublicCollectionUID(token.current);
try {
startLoadingBar();
const collection = await getPublicCollection(
@ -134,17 +148,54 @@ export default function PublicCollectionGallery() {
collectionKey.current
);
setPublicCollection(collection);
await syncPublicFiles(token.current, collection, setPublicFiles);
const isPasswordProtected =
collection?.publicURLs?.[0]?.passwordEnabled;
setIsPasswordProtected(isPasswordProtected);
setErrorMessage(null);
// remove outdated password, sharer has disabled the password
if (!isPasswordProtected && passwordJWTToken.current) {
passwordJWTToken.current = null;
savePublicCollectionPassword(collectionUID, null);
}
if (
!isPasswordProtected ||
(isPasswordProtected && passwordJWTToken.current)
) {
try {
await syncPublicFiles(
token.current,
passwordJWTToken.current,
collection,
setPublicFiles
);
} catch (e) {
const parsedError = parseSharingErrorCodes(e);
if (parsedError.message === CustomError.TOKEN_EXPIRED) {
// passwordToken has expired, sharer has changed the password,
// so,clearing local cache token value to prompt user to re-enter password
passwordJWTToken.current = null;
}
}
}
if (isPasswordProtected && !passwordJWTToken.current) {
await removePublicFiles(collectionUID);
}
} catch (e) {
const parsedError = parseSharingErrorCodes(e);
if (parsedError.message === CustomError.TOKEN_EXPIRED) {
setErrorMessage(constants.LINK_EXPIRED);
if (
parsedError.message === CustomError.TOKEN_EXPIRED ||
parsedError.message === CustomError.TOO_MANY_REQUESTS
) {
setErrorMessage(
parsedError.message === CustomError.TOO_MANY_REQUESTS
? constants.LINK_TOO_MANY_REQUESTS
: constants.LINK_EXPIRED
);
// share has been disabled
// local cache should be cleared
removePublicCollectionWithFiles(
token.current,
collectionUID,
collectionKey.current
);
setPublicCollection(null);
@ -155,16 +206,84 @@ export default function PublicCollectionGallery() {
}
};
if (!publicFiles && loading) {
return <Loader />;
}
const verifyLinkPassword = async (password, setFieldError) => {
try {
const cryptoWorker = await new CryptoWorker();
let hashedPassword: string = null;
try {
const publicUrl = publicCollection.publicURLs[0];
hashedPassword = await cryptoWorker.deriveKey(
password,
publicUrl.nonce,
publicUrl.opsLimit,
publicUrl.memLimit
);
} catch (e) {
logError(e, 'failed to derive key for verifyLinkPassword');
setFieldError(
'passphrase',
`${constants.UNKNOWN_ERROR} ${e.message}`
);
return;
}
const collectionUID = getPublicCollectionUID(token.current);
try {
const jwtToken = await verifyPublicCollectionPassword(
token.current,
hashedPassword
);
passwordJWTToken.current = jwtToken;
savePublicCollectionPassword(collectionUID, jwtToken);
} catch (e) {
const parsedError = parseSharingErrorCodes(e);
if (parsedError.message === CustomError.TOKEN_EXPIRED) {
setFieldError('passphrase', constants.INCORRECT_PASSPHRASE);
return;
}
throw e;
}
await syncWithRemote();
finishLoadingBar();
} catch (e) {
logError(e, 'failed to verifyLinkPassword');
setFieldError(
'passphrase',
`${constants.UNKNOWN_ERROR} ${e.message}`
);
}
};
if (errorMessage && !loading) {
return <Container>{errorMessage}</Container>;
}
if (!publicFiles && !loading) {
return <Container>{constants.NOT_FOUND}</Container>;
if (loading) {
if (!publicFiles) {
return <Loader />;
}
} else {
if (errorMessage) {
return <Container>{errorMessage}</Container>;
}
if (isPasswordProtected && !passwordJWTToken.current) {
return (
<Container>
<Card style={{ maxWidth: '332px' }} className="text-center">
<Card.Body style={{ padding: '40px 30px' }}>
<Card.Subtitle style={{ marginBottom: '2rem' }}>
{/* <LogoImg src="/icon.svg" /> */}
{constants.LINK_PASSWORD}
</Card.Subtitle>
<SingleInputForm
callback={verifyLinkPassword}
placeholder={constants.RETURN_PASSPHRASE_HINT}
buttonText={'unlock'}
fieldType="password"
/>
</Card.Body>
</Card>
</Container>
);
}
if (!publicFiles) {
return <Container>{constants.NOT_FOUND}</Container>;
}
}
return (
@ -172,13 +291,13 @@ export default function PublicCollectionGallery() {
value={{
...defaultPublicCollectionGalleryContext,
token: token.current,
passwordToken: passwordJWTToken.current,
accessedThroughSharedURL: true,
setDialogMessage,
openReportForm,
}}>
<LoadingBar color="#51cd7c" ref={loadingBar} />
<CollectionInfo collection={publicCollection} />
<PhotoFrame
files={publicFiles}
setFiles={setPublicFiles}
@ -194,6 +313,9 @@ export default function PublicCollectionGallery() {
deleted={[]}
activeCollection={ALL_SECTION}
isSharedCollection
enableDownload={
publicCollection?.publicURLs?.[0]?.enableDownload ?? true
}
/>
<AbuseReportForm
show={abuseReportFormView}

View file

@ -19,7 +19,11 @@ class PublicCollectionDownloadManager {
private fileObjectURLPromise = new Map<string, Promise<string>>();
private thumbnailObjectURLPromise = new Map<number, Promise<string>>();
public async getThumbnail(file: EnteFile, token: string) {
public async getThumbnail(
file: EnteFile,
token: string,
passwordToken: string
) {
try {
if (!token) {
return null;
@ -42,7 +46,11 @@ class PublicCollectionDownloadManager {
if (cacheResp) {
return URL.createObjectURL(await cacheResp.blob());
}
const thumb = await this.downloadThumb(token, file);
const thumb = await this.downloadThumb(
token,
passwordToken,
file
);
const thumbBlob = new Blob([thumb]);
try {
await thumbnailCache?.put(
@ -65,11 +73,18 @@ class PublicCollectionDownloadManager {
}
}
private downloadThumb = async (token: string, file: EnteFile) => {
private downloadThumb = async (
token: string,
passwordToken: string,
file: EnteFile
) => {
const resp = await HTTPService.get(
getPublicCollectionThumbnailURL(file.id),
null,
{ 'X-Auth-Access-Token': token },
{
'X-Auth-Access-Token': token,
'X-Auth-Access-Token-JWT': passwordToken,
},
{ responseType: 'arraybuffer' }
);
if (typeof resp.data === 'undefined') {
@ -84,14 +99,23 @@ class PublicCollectionDownloadManager {
return decrypted;
};
getFile = async (file: EnteFile, token: string, forPreview = false) => {
getFile = async (
file: EnteFile,
token: string,
passwordToken: string,
forPreview = false
) => {
const shouldBeConverted = forPreview && needsConversionForPreview(file);
const fileKey = shouldBeConverted
? `${file.id}_converted`
: `${file.id}`;
try {
const getFilePromise = async (convert: boolean) => {
const fileStream = await this.downloadFile(token, file);
const fileStream = await this.downloadFile(
token,
passwordToken,
file
);
let fileBlob = await new Response(fileStream).blob();
if (convert) {
fileBlob = await convertForPreview(file, fileBlob);
@ -117,7 +141,7 @@ class PublicCollectionDownloadManager {
return await this.fileObjectURLPromise.get(file.id.toString());
}
async downloadFile(token: string, file: EnteFile) {
async downloadFile(token: string, passwordToken: string, file: EnteFile) {
const worker = await new CryptoWorker();
if (!token) {
return null;
@ -129,7 +153,10 @@ class PublicCollectionDownloadManager {
const resp = await HTTPService.get(
getPublicCollectionFileURL(file.id),
null,
{ 'X-Auth-Access-Token': token },
{
'X-Auth-Access-Token': token,
'X-Auth-Access-Token-JWT': passwordToken,
},
{ responseType: 'arraybuffer' }
);
if (typeof resp.data === 'undefined') {

View file

@ -12,6 +12,7 @@ import {
} from 'types/publicCollection';
import CryptoWorker from 'utils/crypto';
import { REPORT_REASON } from 'constants/publicCollection';
import { CustomError, parseSharingErrorCodes } from 'utils/error';
const ENDPOINT = getEndpoint();
const PUBLIC_COLLECTION_FILES_TABLE = 'public-collection-files';
@ -22,6 +23,9 @@ export const getPublicCollectionUID = (token: string) => `${token}`;
const getPublicCollectionSyncTimeUID = (collectionUID: string) =>
`public-${collectionUID}-time`;
const getPublicCollectionPasswordKey = (collectionUID: string) =>
`public-${collectionUID}-passkey`;
export const getLocalPublicFiles = async (collectionUID: string) => {
const localSavedPublicCollectionFiles =
(
@ -55,6 +59,26 @@ export const savePublicCollectionFiles = async (
);
};
export const getLocalPublicCollectionPassword = async (
collectionUID: string
): Promise<string> => {
return (
(await localForage.getItem<string>(
getPublicCollectionPasswordKey(collectionUID)
)) || ''
);
};
export const savePublicCollectionPassword = async (
collectionUID: string,
passToken: string
): Promise<string> => {
return await localForage.setItem<string>(
getPublicCollectionPasswordKey(collectionUID),
passToken
);
};
export const getLocalPublicCollection = async (collectionKey: string) => {
const localCollections =
(await localForage.getItem<Collection[]>(PUBLIC_COLLECTIONS_TABLE)) ||
@ -119,6 +143,7 @@ const setPublicCollectionLastSyncTime = async (
export const syncPublicFiles = async (
token: string,
passwordToken: string,
collection: Collection,
setPublicFiles: (files: EnteFile[]) => void
) => {
@ -139,6 +164,7 @@ export const syncPublicFiles = async (
}
const fetchedFiles = await getPublicFiles(
token,
passwordToken,
collection,
lastSyncTime,
files,
@ -171,7 +197,12 @@ export const syncPublicFiles = async (
);
setPublicFiles([...sortFiles(mergeMetadata(files))]);
} catch (e) {
const parsedError = parseSharingErrorCodes(e);
logError(e, 'failed to sync shared collection files');
if (parsedError.message === CustomError.TOKEN_EXPIRED) {
console.log('invalid token or password');
throw e;
}
}
return [...sortFiles(mergeMetadata(files))];
} catch (e) {
@ -182,6 +213,7 @@ export const syncPublicFiles = async (
const getPublicFiles = async (
token: string,
passwordToken: string,
collection: Collection,
sinceTime: number,
files: EnteFile[],
@ -203,6 +235,7 @@ const getPublicFiles = async (
{
'Cache-Control': 'no-cache',
'X-Auth-Access-Token': token,
'X-Auth-Access-Token-JWT': passwordToken,
}
);
decryptedFiles.push(
@ -249,7 +282,7 @@ export const getPublicCollection = async (
null,
{ 'Cache-Control': 'no-cache', 'X-Auth-Access-Token': token }
);
const fetchedCollection = resp.data?.collection;
const fetchedCollection = resp.data.collection;
const collectionName = await decryptCollectionName(
fetchedCollection,
collectionKey
@ -267,6 +300,25 @@ export const getPublicCollection = async (
}
};
export const verifyPublicCollectionPassword = async (
token: string,
passwordHash: string
): Promise<string> => {
try {
const resp = await HTTPService.post(
`${ENDPOINT}/public-collection/verify-password`,
{ passHash: passwordHash },
null,
{ 'Cache-Control': 'no-cache', 'X-Auth-Access-Token': token }
);
const jwtToken = resp.data.jwtToken;
return jwtToken;
} catch (e) {
logError(e, 'failed to verify public collection password');
throw e;
}
};
const decryptCollectionName = async (
collection: Collection,
collectionKey: string
@ -307,10 +359,9 @@ export const reportAbuse = async (
};
export const removePublicCollectionWithFiles = async (
token: string,
collectionUID: string,
collectionKey: string
) => {
const collectionUID = getPublicCollectionUID(token);
const publicCollections =
(await localForage.getItem<Collection[]>(PUBLIC_COLLECTIONS_TABLE)) ||
[];
@ -320,7 +371,11 @@ export const removePublicCollectionWithFiles = async (
(collection) => collection.key !== collectionKey
)
);
await removePublicFiles(collectionUID);
};
export const removePublicFiles = async (collectionUID: string) => {
await localForage.removeItem(getPublicCollectionPasswordKey(collectionUID));
await localForage.removeItem(getPublicCollectionSyncTimeUID(collectionUID));
const publicCollectionFiles =

View file

@ -24,6 +24,11 @@ export interface PublicURL {
url: string;
deviceLimit: number;
validTill: number;
enableDownload: boolean;
passwordEnabled: boolean;
nonce: string;
opsLimit: number;
memLimit: number;
}
export interface CreatePublicAccessTokenRequest {

View file

@ -4,6 +4,7 @@ import { EnteFile } from 'types/file';
export interface PublicCollectionGalleryContextType {
token: string;
passwordToken: string;
accessedThroughSharedURL: boolean;
setDialogMessage: SetDialogMessage;
openReportForm: () => void;

View file

@ -35,6 +35,7 @@ export enum CustomError {
REQUEST_CANCELLED = 'request canceled',
REQUEST_FAILED = 'request failed',
TOKEN_EXPIRED = 'token expired',
TOO_MANY_REQUESTS = 'too many requests',
BAD_REQUEST = 'bad request',
SUBSCRIPTION_NEEDED = 'subscription not present',
NOT_FOUND = 'not found ',
@ -121,9 +122,11 @@ export const parseSharingErrorCodes = (error) => {
break;
case ServerErrorCodes.SESSION_EXPIRED:
case ServerErrorCodes.TOKEN_EXPIRED:
case ServerErrorCodes.TOO_MANY_REQUEST:
parsedMessage = CustomError.TOKEN_EXPIRED;
break;
case ServerErrorCodes.TOO_MANY_REQUEST:
parsedMessage = CustomError.TOO_MANY_REQUESTS;
break;
default:
parsedMessage = `${constants.UNKNOWN_ERROR} statusCode:${errorCode}`;
}

View file

@ -44,7 +44,8 @@ export function downloadAsFile(filename: string, content: string) {
export async function downloadFile(
file: EnteFile,
accessedThroughSharedURL: boolean,
token?: string
token?: string,
passwordToken?: string
) {
let fileURL: string;
let tempURL: string;
@ -58,6 +59,7 @@ export async function downloadFile(
await new Response(
await PublicCollectionDownloadManager.downloadFile(
token,
passwordToken,
file
)
).blob()

View file

@ -4,6 +4,7 @@ import { PublicCollectionGalleryContextType } from 'types/publicCollection';
export const defaultPublicCollectionGalleryContext: PublicCollectionGalleryContextType =
{
token: null,
passwordToken: null,
accessedThroughSharedURL: false,
setDialogMessage: () => null,
openReportForm: () => null,

View file

@ -74,6 +74,7 @@ const englishConstants = {
SENDING: 'sending...',
SENT: 'sent!',
PASSWORD: 'password',
LINK_PASSWORD: 'enter password to unlock the album',
ENTER_PASSPHRASE: 'enter your password',
RETURN_PASSPHRASE_HINT: 'password',
SET_PASSPHRASE: 'set password',
@ -627,7 +628,8 @@ const englishConstants = {
ALBUM_URL: 'album url',
PUBLIC_SHARING: 'link sharing',
NOT_FOUND: '404 - not found',
LINK_EXPIRED: 'the link has expired!',
LINK_EXPIRED: 'this link has either expired or been disabled!',
LINK_TOO_MANY_REQUESTS: 'this album is too popular for us to handle!',
DISABLE_PUBLIC_SHARING: "'disable public sharing",
DISABLE_PUBLIC_SHARING_MESSAGE:
'are you sure you want to disable public sharing?',