🔒 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:
parent
83b1603202
commit
4b8c7eefae
@ -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;
|
||||
}
|
||||
});
|
||||
|
@ -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);
|
||||
}
|
||||
})
|
||||
|
@ -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);
|
||||
}
|
||||
})
|
||||
|
@ -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);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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(),
|
||||
|
@ -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;
|
||||
},
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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 {
|
||||
|
@ -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`;
|
||||
|
@ -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$/);
|
||||
});
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user