Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
211 changes: 96 additions & 115 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
"graphql": "16.13.2",
"graphql-list-fields": "2.0.4",
"graphql-relay": "0.10.2",
"graphql-upload": "15.0.2",
"graphql-upload": "17.0.0",
"intersect": "1.0.1",
"jsonwebtoken": "9.0.3",
"jwks-rsa": "3.2.0",
Expand Down
65 changes: 65 additions & 0 deletions spec/ParseGraphQLServer.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -7465,6 +7465,71 @@ describe('ParseGraphQLServer', () => {
expect(res.status).toEqual(200);
expect(await res.text()).toEqual('My File Content');
});

it('should preserve accented characters in uploaded filenames', async () => {
const clientMutationId = uuidv4();

parseServer = await global.reconfigureServer({
publicServerURL: 'http://localhost:13377/parse',
});
await createGQLFromParseServer(parseServer);
const body = new FormData();
body.append(
'operations',
JSON.stringify({
query: `
mutation CreateFile($input: CreateFileInput!) {
createFile(input: $input) {
clientMutationId
fileInfo {
name
url
}
}
}
`,
variables: {
input: {
clientMutationId,
upload: null,
},
},
})
);
body.append('map', JSON.stringify({ 1: ['variables.input.upload'] }));
body.append('1', 'My File Content', {
filename: 'café.txt',
contentType: 'text/plain',
});

let res = await fetch('http://localhost:13377/graphql', {
method: 'POST',
headers,
body,
});

expect(res.status).toEqual(200);

const result = JSON.parse(await res.text());

expect(result.errors).toBeUndefined();
expect(result.data?.createFile).not.toBeNull();
if (result.errors || !result.data?.createFile) {
return;
}
expect(result.data.createFile.clientMutationId).toEqual(clientMutationId);
expect(result.data.createFile.fileInfo.name).toEqual(
jasmine.stringMatching(/_café.txt$/)
);
expect(result.data.createFile.fileInfo.url).toEqual(
jasmine.stringMatching(/_caf%C3%A9.txt$/)
);

res = await fetch(result.data.createFile.fileInfo.url);

expect(res.status).toEqual(200);
expect(await res.text()).toEqual('My File Content');
});
});
});

Expand Down
2 changes: 1 addition & 1 deletion src/Adapters/Files/FilesAdapter.js
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ export function validateFilename(filename): ?Parse.Error {
return new Parse.Error(Parse.Error.INVALID_FILE_NAME, 'Filename too long.');
}

const regx = /^[_a-zA-Z0-9][a-zA-Z0-9@. ~_-]*$/;
const regx = /^[_\p{L}\p{N}][\p{L}\p{M}\p{N}@. ~_-]*$/u;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
Comment thread
coratgerl marked this conversation as resolved.
Outdated
if (!filename.match(regx)) {
return new Parse.Error(Parse.Error.INVALID_FILE_NAME, 'Filename contains invalid characters.');
}
Expand Down
2 changes: 1 addition & 1 deletion src/GraphQL/ParseGraphQLSchema.js
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ class ParseGraphQLSchema {
this.graphQLSchemaDirectives = {};
this.relayNodeInterface = null;

defaultGraphQLTypes.load(this);
await defaultGraphQLTypes.load(this);
defaultRelaySchema.load(this);
schemaTypes.load(this);

Expand Down
4 changes: 2 additions & 2 deletions src/GraphQL/ParseGraphQLServer.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import graphqlUploadExpress from 'graphql-upload/graphqlUploadExpress.js';
import { ApolloServer } from '@apollo/server';
import { expressMiddleware } from '@as-integrations/express5';
import { ApolloServerPluginCacheControlDisabled } from '@apollo/server/plugin/disabled';
Expand All @@ -10,6 +9,7 @@ import defaultLogger from '../logger';
import { ParseGraphQLSchema } from './ParseGraphQLSchema';
import ParseGraphQLController, { ParseGraphQLConfig } from '../Controllers/ParseGraphQLController';
import { createComplexityValidationPlugin } from './helpers/queryComplexity';
import { createGraphQLUploadMiddleware } from './helpers/graphqlUpload';


const hasTypeIntrospection = (query) => {
Expand Down Expand Up @@ -236,7 +236,7 @@ class ParseGraphQLServer {
app.use(this.config.graphQLPath, handleParseErrors);
app.use(
this.config.graphQLPath,
graphqlUploadExpress({
createGraphQLUploadMiddleware({
maxFileSize: this._transformMaxUploadSizeToBytes(
this.parseServer.config.maxUploadSize || '20mb'
),
Expand Down
51 changes: 51 additions & 0 deletions src/GraphQL/helpers/graphqlUpload.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// Cache the dynamic imports so the ESM-only graphql-upload modules are
// resolved once and then reused by both schema loading and request handling.
let graphqlUploadModulesPromise;

const loadGraphQLUploadModules = async () => {
if (!graphqlUploadModulesPromise) {
graphqlUploadModulesPromise = Promise.all([
import('graphql-upload/GraphQLUpload.mjs'),
import('graphql-upload/processRequest.mjs'),
]).then(([{ default: GraphQLUpload }, { default: processRequest }]) => ({
GraphQLUpload,
processRequest,
}));
}

return graphqlUploadModulesPromise;
};

// Expose the Upload scalar lazily so the rest of Parse Server can stay on the
// current module system while graphql-upload is now ESM-only.
const getGraphQLUpload = async () => {
const { GraphQLUpload } = await loadGraphQLUploadModules();
return GraphQLUpload;
};

const createGraphQLUploadMiddleware = options => {
const uploadOptions = {
...options,
// Decode multipart filename parameters as UTF-8 so filenames like
// "cafe.txt" with accents don't arrive as mojibake.
defParamCharset: 'utf8',
};

return async (req, res, next) => {
if (!req.is || !req.is('multipart/form-data')) {
return next();
}

try {
const { processRequest } = await loadGraphQLUploadModules();
// graphql-upload parses the multipart body and populates req.body with
// Upload promises before Apollo handles the GraphQL operation.
req.body = await processRequest(req, res, uploadOptions);
return next();
} catch (error) {
return next(error);
}
};
};

export { createGraphQLUploadMiddleware, getGraphQLUpload };
40 changes: 24 additions & 16 deletions src/GraphQL/loaders/defaultGraphQLTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ import {
GraphQLUnionType,
} from 'graphql';
import { toGlobalId } from 'graphql-relay';
import GraphQLUpload from 'graphql-upload/GraphQLUpload.js';
import Utils from '../../Utils';
import { getGraphQLUpload } from '../helpers/graphqlUpload';

class TypeValidationError extends Error {
constructor(value, type) {
Expand Down Expand Up @@ -356,21 +356,25 @@ const FILE_INFO = new GraphQLObjectType({
},
});

const FILE_INPUT = new GraphQLInputObjectType({
name: 'FileInput',
description:
'If this field is set to null the file will be unlinked (the file will not be deleted on cloud storage).',
fields: {
file: {
description: 'A File Scalar can be an url or a FileInfo object.',
type: FILE,
},
upload: {
description: 'Use this field if you want to create a new file.',
type: GraphQLUpload,
let GraphQLUpload;
let FILE_INPUT;

const createFileInputType = graphQLUpload =>
new GraphQLInputObjectType({
name: 'FileInput',
description:
'If this field is set to null the file will be unlinked (the file will not be deleted on cloud storage).',
fields: {
file: {
description: 'A File Scalar can be an url or a FileInfo object.',
type: FILE,
},
upload: {
description: 'Use this field if you want to create a new file.',
type: graphQLUpload,
},
},
},
});
});

const GEO_POINT_FIELDS = {
latitude: {
Expand Down Expand Up @@ -1219,7 +1223,11 @@ const loadArrayResult = (parseGraphQLSchema, parseClassesArray) => {
parseGraphQLSchema.graphQLTypes.push(ARRAY_RESULT);
};

const load = parseGraphQLSchema => {
const load = async parseGraphQLSchema => {
GraphQLUpload = await getGraphQLUpload();
if (!FILE_INPUT) {
FILE_INPUT = createFileInputType(GraphQLUpload);
}
parseGraphQLSchema.addGraphQLType(GraphQLUpload, true);
parseGraphQLSchema.addGraphQLType(ANY, true);
parseGraphQLSchema.addGraphQLType(OBJECT, true);
Expand Down
15 changes: 13 additions & 2 deletions src/GraphQL/loaders/filesMutations.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,14 @@ const handleUpload = async (upload, config) => {
try {
const ext = mime.getExtension(mimetype);
const fullFileName = filename.endsWith(`.${ext}`) ? filename : `${filename}.${ext}`;
const encodedFileName = fullFileName.split('/').map(encodeURIComponent).join('/');
const serverUrl = new URL(config.serverURL);
const fileInfo = await new Promise((resolve, reject) => {
const req = request(
{
hostname: serverUrl.hostname,
port: serverUrl.port,
path: `${serverUrl.pathname}/files/${fullFileName}`,
path: `${serverUrl.pathname}/files/${encodedFileName}`,
method: 'POST',
headers,
},
Expand All @@ -37,7 +38,17 @@ const handleUpload = async (upload, config) => {
});
res.on('end', () => {
try {
resolve(JSON.parse(data));
const parsedData = JSON.parse(data);
if (res.statusCode < 200 || res.statusCode >= 400) {
reject(
new Parse.Error(
parsedData.code || Parse.Error.FILE_SAVE_ERROR,
parsedData.error || data
)
);
return;
}
resolve(parsedData);
} catch {
reject(new Parse.Error(Parse.error, data));
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
}
Expand Down
Loading