🔒 Anonymised the name of edited images

refs [ENG-1260](https://linear.app/tryghost/issue/ENG-1260/🔒-redacting-pictures-in-pintura-leaves-easily-findable-original-image)
This commit is contained in:
Michael Barrett 2024-07-10 16:31:29 +01:00
parent 83b1603202
commit 4b8c7eefae
No known key found for this signature in database
11 changed files with 78 additions and 18 deletions

View File

@ -7,13 +7,20 @@ export interface ImagesResponseType {
}[];
}
export const useUploadImage = createMutation<ImagesResponseType, {file: File}>({
// eslint-disable-next-line no-shadow
export enum ImageStatus {
NEW = 'new',
EDITED = 'edited',
}
export const useUploadImage = createMutation<ImagesResponseType, {file: File, status?: ImageStatus}>({
method: 'POST',
path: () => '/images/upload/',
body: ({file}) => {
body: ({file, status = ImageStatus.NEW}) => {
const formData = new FormData();
formData.append('file', file);
formData.append('purpose', 'image');
formData.append('status', status);
return formData;
}
});

View File

@ -4,7 +4,7 @@ import usePinturaEditor from '../../../hooks/usePinturaEditor';
import useSettingGroup from '../../../hooks/useSettingGroup';
import {APIError} from '@tryghost/admin-x-framework/errors';
import {FacebookLogo, ImageUpload, SettingGroupContent, TextField, withErrorBoundary} from '@tryghost/admin-x-design-system';
import {getImageUrl, useUploadImage} from '@tryghost/admin-x-framework/api/images';
import {getImageUrl, ImageStatus, useUploadImage} from '@tryghost/admin-x-framework/api/images';
import {getSettingValues} from '@tryghost/admin-x-framework/api/settings';
import {useHandleError} from '@tryghost/admin-x-framework/hooks';
@ -86,7 +86,7 @@ const Facebook: React.FC<{ keywords: string[] }> = ({keywords}) => {
openEditor: async () => editor.openEditor({
image: facebookImage || '',
handleSave: async (file:File) => {
const imageUrl = getImageUrl(await uploadImage({file}));
const imageUrl = getImageUrl(await uploadImage({file, status: ImageStatus.EDITED}));
updateSetting('og_image', imageUrl);
}
})

View File

@ -4,7 +4,7 @@ import usePinturaEditor from '../../../hooks/usePinturaEditor';
import useSettingGroup from '../../../hooks/useSettingGroup';
import {APIError} from '@tryghost/admin-x-framework/errors';
import {ImageUpload, SettingGroupContent, TextField, XLogo, withErrorBoundary} from '@tryghost/admin-x-design-system';
import {getImageUrl, useUploadImage} from '@tryghost/admin-x-framework/api/images';
import {getImageUrl, ImageStatus, useUploadImage} from '@tryghost/admin-x-framework/api/images';
import {getSettingValues} from '@tryghost/admin-x-framework/api/settings';
import {useHandleError} from '@tryghost/admin-x-framework/hooks';
@ -82,7 +82,7 @@ const Twitter: React.FC<{ keywords: string[] }> = ({keywords}) => {
openEditor: async () => editor.openEditor({
image: twitterImage || '',
handleSave: async (file:File) => {
const imageUrl = getImageUrl(await uploadImage({file}));
const imageUrl = getImageUrl(await uploadImage({file, status: ImageStatus.EDITED}));
updateSetting('twitter_image', imageUrl);
}
})

View File

@ -15,7 +15,7 @@ import {ErrorMessages, useForm, useHandleError} from '@tryghost/admin-x-framewor
import {HostLimitError, useLimiter} from '../../../hooks/useLimiter';
import {RoutingModalProps, useRouting} from '@tryghost/admin-x-framework/routing';
import {User, canAccessSettings, hasAdminAccess, isAdminUser, isAuthorOrContributor, isEditorUser, isOwnerUser, useDeleteUser, useEditUser, useMakeOwner} from '@tryghost/admin-x-framework/api/users';
import {getImageUrl, useUploadImage} from '@tryghost/admin-x-framework/api/images';
import {getImageUrl, ImageStatus, useUploadImage} from '@tryghost/admin-x-framework/api/images';
import {useGlobalData} from '../../providers/GlobalDataProvider';
import {validateFacebookUrl, validateTwitterUrl} from '../../../utils/socialUrls';
@ -246,9 +246,9 @@ const UserDetailModalContent: React.FC<{user: User}> = ({user}) => {
});
};
const handleImageUpload = async (image: string, file: File) => {
const handleImageUpload = async (image: string, file: File, status: ImageStatus = ImageStatus.NEW) => {
try {
const imageUrl = getImageUrl(await uploadImage({file}));
const imageUrl = getImageUrl(await uploadImage({file, status}));
switch (image) {
case 'cover_image':
@ -383,7 +383,7 @@ const UserDetailModalContent: React.FC<{user: User}> = ({user}) => {
openEditor: async () => editor.openEditor({
image: formState.profile_image || '',
handleSave: async (file:File) => {
handleImageUpload('profile_image', file);
handleImageUpload('profile_image', file, ImageStatus.EDITED);
}
})
}
@ -421,7 +421,7 @@ const UserDetailModalContent: React.FC<{user: User}> = ({user}) => {
openEditor: async () => editor.openEditor({
image: formState.cover_image || '',
handleSave: async (file:File) => {
handleImageUpload('cover_image', file);
handleImageUpload('cover_image', file, ImageStatus.EDITED);
}
})
}

View File

@ -131,7 +131,7 @@ const BrandSettings: React.FC<{ values: BrandSettingValues, updateSetting: (key:
image: values.coverImage || '',
handleSave: async (file:File) => {
try {
updateSetting('cover_image', getImageUrl(await uploadImage({file})));
updateSetting('cover_image', getImageUrl(await uploadImage({file, original: values.coverImage})));
} catch (e) {
handleError(e);
}

View File

@ -15,13 +15,16 @@ import {isBlank} from '@ember/utils';
import {run} from '@ember/runloop';
import {inject as service} from '@ember/service';
export const IMAGE_STATUS_NEW = 'new';
export const IMAGE_STATUS_EDITED = 'edited';
export const IMAGE_MIME_TYPES = 'image/gif,image/jpg,image/jpeg,image/png,image/svg+xml,image/webp';
export const IMAGE_EXTENSIONS = ['gif', 'jpg', 'jpeg', 'png', 'svg', 'svgz', 'webp'];
export const IMAGE_PARAMS = {purpose: 'image'};
export const IMAGE_PARAMS = {purpose: 'image', status: IMAGE_STATUS_NEW};
export const ICON_EXTENSIONS = ['gif', 'ico', 'jpg', 'jpeg', 'png', 'svg', 'svgz', 'webp'];
export const ICON_MIME_TYPES = 'image/x-icon,image/vnd.microsoft.icon,image/gif,image/jpg,image/jpeg,image/png,image/svg+xml,image/webp';
export const ICON_PARAMS = {purpose: 'icon'};
export const ICON_PARAMS = {purpose: 'icon', status: IMAGE_STATUS_NEW};
export default Component.extend({
ajax: service(),

View File

@ -6,7 +6,9 @@ import {
ICON_EXTENSIONS,
ICON_MIME_TYPES,
IMAGE_EXTENSIONS,
IMAGE_MIME_TYPES
IMAGE_MIME_TYPES,
IMAGE_STATUS_EDITED,
IMAGE_STATUS_NEW
} from 'ghost-admin/components/gh-image-uploader';
import {all, task} from 'ember-concurrency';
import {isArray} from '@ember/array';
@ -95,7 +97,7 @@ export default Component.extend({
this._uploadTrackers = [];
if (!this.paramsHash) {
this.set('paramsHash', {purpose: 'image'});
this.set('paramsHash', {purpose: 'image', status: IMAGE_STATUS_NEW});
}
this.set('imageExtensions', IMAGE_EXTENSIONS);
@ -333,6 +335,10 @@ export default Component.extend({
formData.append(key, this.paramsHash[key]);
});
if (file.edited) {
formData.set('status', IMAGE_STATUS_EDITED);
}
return formData;
},

View File

@ -230,6 +230,7 @@ export default class KoenigImageEditor extends Component {
editor.on('process', (result) => {
// save edited image
result.dest.edited = true;
try {
if (this.args.saveImage) {
this.args.saveImage(result.dest);

View File

@ -9,12 +9,16 @@ import {didCancel, task} from 'ember-concurrency';
import {inject} from 'ghost-admin/decorators/inject';
import {inject as service} from '@ember/service';
const IMAGE_STATUS_NEW = 'new';
const IMAGE_STATUS_EDITED = 'edited';
export const fileTypes = {
image: {
mimeTypes: ['image/gif', 'image/jpg', 'image/jpeg', 'image/png', 'image/svg+xml', 'image/webp'],
extensions: ['gif', 'jpg', 'jpeg', 'png', 'svg', 'svgz', 'webp'],
endpoint: '/images/upload/',
resourceName: 'images'
resourceName: 'images',
uploadParams: {purpose: 'image', status: IMAGE_STATUS_NEW}
},
video: {
mimeTypes: ['video/mp4', 'video/webm', 'video/ogg'],
@ -537,10 +541,16 @@ export default class KoenigLexicalEditor extends Component {
const fileFormData = new FormData();
fileFormData.append('file', file, file.name);
formData = {...fileTypes[type].uploadParams, ...formData};
Object.keys(formData || {}).forEach((key) => {
fileFormData.append(key, formData[key]);
});
if (type === 'image' && file.edited) {
fileFormData.set('status', IMAGE_STATUS_EDITED);
}
const url = `${ghostPaths().apiRoot}${fileTypes[type].endpoint}`;
try {

View File

@ -1,11 +1,14 @@
/* eslint-disable ghost/ghost-custom/max-api-complexity */
const path = require('path');
const uuid = require('uuid');
const errors = require('@tryghost/errors');
const imageTransform = require('@tryghost/image-transform');
const storage = require('../../adapters/storage');
const config = require('../../../shared/config');
const IMAGE_STATUS_EDITED = 'edited';
/** @type {import('@tryghost/api-framework').Controller} */
const controller = {
docName: 'images',
@ -24,6 +27,13 @@ const controller = {
// Trim _o from file name (not allowed suffix)
frame.file.name = frame.file.name.replace(/_o(\.\w+?)$/, '$1');
// If this an edited version of an existing image, anonymise the name
if (frame.data.status === IMAGE_STATUS_EDITED) {
const ext = path.extname(frame.file.name);
frame.file.name = `${uuid.v4()}${ext}`;
}
// CASE: image transform is not capable of transforming file (e.g. .gif)
if (imageTransform.shouldResizeFileExtension(frame.file.ext) && imageOptimizationOptions.resize) {
const out = `${frame.file.path}_processed`;

View File

@ -14,6 +14,9 @@ const {imageSize} = require('../../../core/server/lib/image');
const configUtils = require('../../utils/configUtils');
const logging = require('@tryghost/logging');
const IMAGE_STATUS_NEW = 'new';
const IMAGE_STATUS_EDITED = 'edited';
const images = [];
let agent, frontendAgent, ghostServer;
/**
@ -23,9 +26,10 @@ let agent, frontendAgent, ghostServer;
* @param {string} options.filename
* @param {string} options.contentType
* @param {string} [options.ref]
* @param {string} [options.status]
* @returns
*/
const uploadImageRequest = ({fileContents, filename, contentType, ref}) => {
const uploadImageRequest = ({fileContents, filename, contentType, ref, status = IMAGE_STATUS_NEW}) => {
const form = new FormData();
form.append('file', fileContents, {
filename,
@ -33,6 +37,8 @@ const uploadImageRequest = ({fileContents, filename, contentType, ref}) => {
});
form.append('purpose', 'image');
form.append('status', status);
if (ref) {
form.append('ref', ref);
}
@ -400,4 +406,21 @@ describe('Images API', function () {
});
sinon.assert.calledOnce(loggingStub);
});
it('Anonymizes the name of an edited image', async function () {
const originalFilePath = p.join(__dirname, '/../../utils/fixtures/images/ghost-logo.png');
const fileContents = await fs.readFile(originalFilePath);
const filename = 'foobarbaz.png';
const {body} = await uploadImageRequest({fileContents, filename, contentType: 'image/png', status: IMAGE_STATUS_EDITED})
.expectStatus(201);
const name = body.images[0].url.split('/').pop();
// Assert that name has been changed
assert.equal(name !== filename, true);
// Assert that name is a UUID v4
assert.match(name, /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}\.png$/);
});
});