diff --git a/.travis.yml b/.travis.yml index 0ea8f9933b..c8716dde14 100644 --- a/.travis.yml +++ b/.travis.yml @@ -40,6 +40,8 @@ before_script: - psql -c 'CREATE EXTENSION postgis;' -U postgres -d parse_server_postgres_adapter_test_database - psql -c 'CREATE EXTENSION postgis_topology;' -U postgres -d parse_server_postgres_adapter_test_database - silent=1 mongodb-runner --start +- npm install -g jsinspect +- jsinspect -t 40 ./src after_script: - bash <(curl -s https://codecov.io/bash) diff --git a/src/AccountLockout.js b/src/AccountLockout.js index 358f1ac402..06887f1b06 100644 --- a/src/AccountLockout.js +++ b/src/AccountLockout.js @@ -33,11 +33,7 @@ export class AccountLockout { return this._config.database.find('_User', query) .then(users => { - if (Array.isArray(users) && users.length > 0) { - return true; - } else { - return false; - } + return Array.isArray(users) && users.length > 0; }); } diff --git a/src/Adapters/Auth/AuthAdapter.js b/src/Adapters/Auth/AuthAdapter.js index dd8fd838c3..7b4ff4cb6f 100644 --- a/src/Adapters/Auth/AuthAdapter.js +++ b/src/Adapters/Auth/AuthAdapter.js @@ -1,4 +1,7 @@ /*eslint no-unused-vars: "off"*/ + +var https = require('https'); + export class AuthAdapter { /* @@ -17,6 +20,42 @@ export class AuthAdapter { validateAuthData(authData, options) { return Promise.resolve({}); } + + /** + * A promisey wrapper for all auth requests + * + * @param {string} name Name of auth to use in rejection message + * @param {Object|string} config Config/String to pass to https.get + * @param {Object|null} postData Optional data to post with + * @returns {Promise} + */ + static request(name, config, postData) { + return new Promise(function(resolve, reject) { + const req = https.get(config, function(res) { + let data = ''; + res.on('data', function(chunk) { + data += chunk; + }); + res.on('end', function() { + try { + data = JSON.parse(data); + } catch(e) { + return reject(e); + } + resolve(data); + }); + }).on('error', function() { + reject('Failed to validate this access token with ' + name + '.'); + }); + if(postData) { + req.on('error', function() { + reject('Failed to validate this access token with ' + name + '.'); + }); + req.write(postData); + req.end(); + } + }); + } } export default AuthAdapter; diff --git a/src/Adapters/Auth/facebook.js b/src/Adapters/Auth/facebook.js index ab846e43e6..b40742c1d4 100644 --- a/src/Adapters/Auth/facebook.js +++ b/src/Adapters/Auth/facebook.js @@ -1,5 +1,6 @@ // Helper functions for accessing the Facebook Graph API. -var https = require('https'); +import AuthAdapter from "./AuthAdapter"; + var Parse = require('parse/node').Parse; // Returns a promise that fulfills iff this user id is valid. @@ -36,24 +37,7 @@ function validateAppId(appIds, authData) { // A promisey wrapper for FB graph requests. function graphRequest(path) { - return new Promise(function(resolve, reject) { - https.get('https://graph.facebook.com/v2.5/' + path, function(res) { - var data = ''; - res.on('data', function(chunk) { - data += chunk; - }); - res.on('end', function() { - try { - data = JSON.parse(data); - } catch(e) { - return reject(e); - } - resolve(data); - }); - }).on('error', function() { - reject('Failed to validate this access token with Facebook.'); - }); - }); + return AuthAdapter.request('Facebook', 'https://graph.facebook.com/v2.5/' + path); } module.exports = { diff --git a/src/Adapters/Auth/github.js b/src/Adapters/Auth/github.js index 146fbdc6f2..2021b04a75 100644 --- a/src/Adapters/Auth/github.js +++ b/src/Adapters/Auth/github.js @@ -1,5 +1,5 @@ // Helper functions for accessing the github API. -var https = require('https'); +import AuthAdapter from "./AuthAdapter"; var Parse = require('parse/node').Parse; // Returns a promise that fulfills iff this user id is valid. @@ -22,30 +22,13 @@ function validateAppId() { // A promisey wrapper for api requests function request(path, access_token) { - return new Promise(function(resolve, reject) { - https.get({ - host: 'api.github.com', - path: '/' + path, - headers: { - 'Authorization': 'bearer ' + access_token, - 'User-Agent': 'parse-server' - } - }, function(res) { - var data = ''; - res.on('data', function(chunk) { - data += chunk; - }); - res.on('end', function() { - try { - data = JSON.parse(data); - } catch(e) { - return reject(e); - } - resolve(data); - }); - }).on('error', function() { - reject('Failed to validate this access token with Github.'); - }); + return AuthAdapter.request('Github', { + host: 'api.github.com', + path: '/' + path, + headers: { + 'Authorization': 'bearer ' + access_token, + 'User-Agent': 'parse-server' + } }); } diff --git a/src/Adapters/Auth/google.js b/src/Adapters/Auth/google.js index 7cc414922a..0d39801082 100644 --- a/src/Adapters/Auth/google.js +++ b/src/Adapters/Auth/google.js @@ -1,29 +1,13 @@ // Helper functions for accessing the google API. -var https = require('https'); -var Parse = require('parse/node').Parse; +import AuthAdapter from "./AuthAdapter"; +const Parse = require('parse/node').Parse; function validateIdToken(id, token) { - return request("tokeninfo?id_token=" + token) - .then((response) => { - if (response && (response.sub == id || response.user_id == id)) { - return; - } - throw new Parse.Error( - Parse.Error.OBJECT_NOT_FOUND, - 'Google auth is invalid for this user.'); - }); + return makeRequest(id, "tokeninfo?id_token=" + token); } function validateAuthToken(id, token) { - return request("tokeninfo?access_token=" + token) - .then((response) => { - if (response && (response.sub == id || response.user_id == id)) { - return; - } - throw new Parse.Error( - Parse.Error.OBJECT_NOT_FOUND, - 'Google auth is invalid for this user.'); - }); + return makeRequest(id, "tokeninfo?access_token=" + token); } // Returns a promise that fulfills if this user id is valid. @@ -46,26 +30,21 @@ function validateAppId() { return Promise.resolve(); } +function makeRequest(id, path) { + return request(path) + .then((response) => { + if (response && (response.sub === id || response.user_id === id)) { + return; + } + throw new Parse.Error( + Parse.Error.OBJECT_NOT_FOUND, + 'Google auth is invalid for this user.'); + }); +} + // A promisey wrapper for api requests function request(path) { - return new Promise(function(resolve, reject) { - https.get("https://www.googleapis.com/oauth2/v3/" + path, function(res) { - var data = ''; - res.on('data', function(chunk) { - data += chunk; - }); - res.on('end', function() { - try { - data = JSON.parse(data); - } catch(e) { - return reject(e); - } - resolve(data); - }); - }).on('error', function() { - reject('Failed to validate this access token with Google.'); - }); - }); + return AuthAdapter.request('Google', 'https://www.googleapis.com/oauth2/v3/' + path); } module.exports = { diff --git a/src/Adapters/Auth/instagram.js b/src/Adapters/Auth/instagram.js index 1c6c0f73cf..debed8ad94 100644 --- a/src/Adapters/Auth/instagram.js +++ b/src/Adapters/Auth/instagram.js @@ -1,12 +1,13 @@ // Helper functions for accessing the instagram API. -var https = require('https'); -var Parse = require('parse/node').Parse; +import AuthAdapter from "./AuthAdapter"; + +const Parse = require('parse/node').Parse; // Returns a promise that fulfills iff this user id is valid. function validateAuthData(authData) { return request("users/self/?access_token=" + authData.access_token) .then((response) => { - if (response && response.data && response.data.id == authData.id) { + if (response && response.data && response.data.id === authData.id) { return; } throw new Parse.Error( @@ -22,20 +23,7 @@ function validateAppId() { // A promisey wrapper for api requests function request(path) { - return new Promise(function(resolve, reject) { - https.get("https://api.instagram.com/v1/" + path, function(res) { - var data = ''; - res.on('data', function(chunk) { - data += chunk; - }); - res.on('end', function() { - data = JSON.parse(data); - resolve(data); - }); - }).on('error', function() { - reject('Failed to validate this access token with Instagram.'); - }); - }); + return AuthAdapter.request('Instagram', "https://api.instagram.com/v1/" + path); } module.exports = { diff --git a/src/Adapters/Auth/linkedin.js b/src/Adapters/Auth/linkedin.js index de5fc66ce5..09ee83249b 100644 --- a/src/Adapters/Auth/linkedin.js +++ b/src/Adapters/Auth/linkedin.js @@ -1,5 +1,5 @@ // Helper functions for accessing the linkedin API. -var https = require('https'); +import AuthAdapter from "./AuthAdapter"; var Parse = require('parse/node').Parse; // Returns a promise that fulfills iff this user id is valid. @@ -22,7 +22,7 @@ function validateAppId() { // A promisey wrapper for api requests function request(path, access_token, is_mobile_sdk) { - var headers = { + const headers = { 'Authorization': 'Bearer ' + access_token, 'x-li-format': 'json', } @@ -31,27 +31,10 @@ function request(path, access_token, is_mobile_sdk) { headers['x-li-src'] = 'msdk'; } - return new Promise(function(resolve, reject) { - https.get({ - host: 'api.linkedin.com', - path: '/v1/' + path, - headers: headers - }, function(res) { - var data = ''; - res.on('data', function(chunk) { - data += chunk; - }); - res.on('end', function() { - try { - data = JSON.parse(data); - } catch(e) { - return reject(e); - } - resolve(data); - }); - }).on('error', function() { - reject('Failed to validate this access token with Linkedin.'); - }); + return AuthAdapter.request('Linkedin', { + host: 'api.linkedin.com', + path: '/v1/' + path, + headers: headers }); } diff --git a/src/Adapters/Auth/meetup.js b/src/Adapters/Auth/meetup.js index bb14dc547b..4eb39a2598 100644 --- a/src/Adapters/Auth/meetup.js +++ b/src/Adapters/Auth/meetup.js @@ -1,5 +1,5 @@ // Helper functions for accessing the meetup API. -var https = require('https'); +import AuthAdapter from "./AuthAdapter"; var Parse = require('parse/node').Parse; // Returns a promise that fulfills iff this user id is valid. @@ -22,29 +22,12 @@ function validateAppId() { // A promisey wrapper for api requests function request(path, access_token) { - return new Promise(function(resolve, reject) { - https.get({ - host: 'api.meetup.com', - path: '/2/' + path, - headers: { - 'Authorization': 'bearer ' + access_token - } - }, function(res) { - var data = ''; - res.on('data', function(chunk) { - data += chunk; - }); - res.on('end', function() { - try { - data = JSON.parse(data); - } catch(e) { - return reject(e); - } - resolve(data); - }); - }).on('error', function() { - reject('Failed to validate this access token with Meetup.'); - }); + return AuthAdapter.request('Meetup', { + host: 'api.meetup.com', + path: '/2/' + path, + headers: { + 'Authorization': 'bearer ' + access_token + } }); } diff --git a/src/Adapters/Auth/spotify.js b/src/Adapters/Auth/spotify.js index 701422c585..dee64e755b 100644 --- a/src/Adapters/Auth/spotify.js +++ b/src/Adapters/Auth/spotify.js @@ -1,5 +1,5 @@ // Helper functions for accessing the Spotify API. -var https = require('https'); +import AuthAdapter from "./AuthAdapter"; var Parse = require('parse/node').Parse; // Returns a promise that fulfills iff this user id is valid. @@ -36,29 +36,12 @@ function validateAppId(appIds, authData) { // A promisey wrapper for Spotify API requests. function request(path, access_token) { - return new Promise(function(resolve, reject) { - https.get({ - host: 'api.spotify.com', - path: '/v1/' + path, - headers: { - 'Authorization': 'Bearer ' + access_token - } - }, function(res) { - var data = ''; - res.on('data', function(chunk) { - data += chunk; - }); - res.on('end', function() { - try { - data = JSON.parse(data); - } catch(e) { - return reject(e); - } - resolve(data); - }); - }).on('error', function() { - reject('Failed to validate this access token with Spotify.'); - }); + return AuthAdapter.request('Spotify', { + host: 'api.spotify.com', + path: '/v1/' + path, + headers: { + 'Authorization': 'Bearer ' + access_token + } }); } diff --git a/src/Adapters/Auth/vkontakte.js b/src/Adapters/Auth/vkontakte.js index 8c9bd5efc8..f8e59089d7 100644 --- a/src/Adapters/Auth/vkontakte.js +++ b/src/Adapters/Auth/vkontakte.js @@ -1,8 +1,7 @@ 'use strict'; // Helper functions for accessing the vkontakte API. - -var https = require('https'); +import AuthAdapter from "./AuthAdapter"; var Parse = require('parse/node').Parse; var logger = require('../../logger').default; @@ -41,24 +40,7 @@ function validateAppId() { // A promisey wrapper for api requests function request(host, path) { - return new Promise(function (resolve, reject) { - https.get("https://" + host + "/" + path, function (res) { - var data = ''; - res.on('data', function (chunk) { - data += chunk; - }); - res.on('end', function () { - try { - data = JSON.parse(data); - } catch(e) { - return reject(e); - } - resolve(data); - }); - }).on('error', function () { - reject('Failed to validate this access token with Vk.'); - }); - }); + return AuthAdapter.request('Vk', 'https://' + host + '/' + path); } module.exports = { diff --git a/src/Adapters/Auth/wechat.js b/src/Adapters/Auth/wechat.js index e42d5c365c..4ff4436619 100644 --- a/src/Adapters/Auth/wechat.js +++ b/src/Adapters/Auth/wechat.js @@ -1,5 +1,5 @@ // Helper functions for accessing the WeChat Graph API. -var https = require('https'); +import AuthAdapter from "./AuthAdapter"; var Parse = require('parse/node').Parse; // Returns a promise that fulfills iff this user id is valid. @@ -19,24 +19,7 @@ function validateAppId() { // A promisey wrapper for WeChat graph requests. function graphRequest(path) { - return new Promise(function (resolve, reject) { - https.get('https://api.weixin.qq.com/sns/' + path, function (res) { - var data = ''; - res.on('data', function (chunk) { - data += chunk; - }); - res.on('end', function () { - try { - data = JSON.parse(data); - } catch(e) { - return reject(e); - } - resolve(data); - }); - }).on('error', function () { - reject('Failed to validate this access token with wechat.'); - }); - }); + return AuthAdapter.request('wechat', 'https://api.weixin.qq.com/sns/' + path); } module.exports = { diff --git a/src/Adapters/Auth/weibo.js b/src/Adapters/Auth/weibo.js index 64efada2f6..75de864ba3 100644 --- a/src/Adapters/Auth/weibo.js +++ b/src/Adapters/Auth/weibo.js @@ -1,5 +1,5 @@ // Helper functions for accessing the weibo Graph API. -var https = require('https'); +import AuthAdapter from "./AuthAdapter"; var Parse = require('parse/node').Parse; var querystring = require('querystring'); @@ -20,42 +20,20 @@ function validateAppId() { // A promisey wrapper for weibo graph requests. function graphRequest(access_token) { - return new Promise(function (resolve, reject) { - var postData = querystring.stringify({ - "access_token":access_token - }); - var options = { - hostname: 'api.weibo.com', - path: '/oauth2/get_token_info', - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - 'Content-Length': Buffer.byteLength(postData) - } - }; - var req = https.request(options, function(res){ - var data = ''; - res.on('data', function (chunk) { - data += chunk; - }); - res.on('end', function () { - try { - data = JSON.parse(data); - } catch(e) { - return reject(e); - } - resolve(data); - }); - res.on('error', function () { - reject('Failed to validate this access token with weibo.'); - }); - }); - req.on('error', function () { - reject('Failed to validate this access token with weibo.'); - }); - req.write(postData); - req.end(); + const postData = querystring.stringify({ + "access_token":access_token }); + const options = { + hostname: 'api.weibo.com', + path: '/oauth2/get_token_info', + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + 'Content-Length': Buffer.byteLength(postData) + } + }; + + return AuthAdapter.request('weibo', options, postData); } module.exports = { diff --git a/src/Adapters/Files/GridStoreAdapter.js b/src/Adapters/Files/GridStoreAdapter.js index 6814297036..5e306551b0 100644 --- a/src/Adapters/Files/GridStoreAdapter.js +++ b/src/Adapters/Files/GridStoreAdapter.js @@ -50,13 +50,17 @@ export class GridStoreAdapter extends FilesAdapter { }); } + openGridStore(database, filename) { + return GridStore.exist(database, filename) + .then(() => { + const gridStore = new GridStore(database, filename, 'r'); + return gridStore.open(); + }); + } + getFileData(filename: string) { return this._connect().then(database => { - return GridStore.exist(database, filename) - .then(() => { - const gridStore = new GridStore(database, filename, 'r'); - return gridStore.open(); - }); + return this.openGridStore(database, filename); }).then(gridStore => { return gridStore.read(); }); @@ -68,10 +72,7 @@ export class GridStoreAdapter extends FilesAdapter { getFileStream(filename: string) { return this._connect().then(database => { - return GridStore.exist(database, filename).then(() => { - const gridStore = new GridStore(database, filename, 'r'); - return gridStore.open(); - }); + return this.openGridStore(database, filename); }); } } diff --git a/src/Adapters/Storage/Mongo/MongoSchemaCollection.js b/src/Adapters/Storage/Mongo/MongoSchemaCollection.js index 051bac65cf..c27568e55c 100644 --- a/src/Adapters/Storage/Mongo/MongoSchemaCollection.js +++ b/src/Adapters/Storage/Mongo/MongoSchemaCollection.js @@ -1,5 +1,6 @@ import MongoCollection from './MongoCollection'; import Parse from 'parse/node'; +import StorageUtils from '../StorageUtils'; function mongoFieldToParseSchemaField(type) { if (type[0] === '*') { @@ -43,28 +44,10 @@ function mongoSchemaFieldsToParseSchemaFields(schema) { return response; } -const emptyCLPS = Object.freeze({ - find: {}, - get: {}, - create: {}, - update: {}, - delete: {}, - addField: {}, -}); - -const defaultCLPS = Object.freeze({ - find: {'*': true}, - get: {'*': true}, - create: {'*': true}, - update: {'*': true}, - delete: {'*': true}, - addField: {'*': true}, -}); - function mongoSchemaToParseSchema(mongoSchema) { - let clps = defaultCLPS; + let clps = StorageUtils.getDefaultCLPs(); if (mongoSchema._metadata && mongoSchema._metadata.class_permissions) { - clps = {...emptyCLPS, ...mongoSchema._metadata.class_permissions}; + clps = {...StorageUtils.getEmptyCLPs(), ...mongoSchema._metadata.class_permissions}; } return { className: mongoSchema._id, diff --git a/src/Adapters/Storage/Mongo/MongoTransform.js b/src/Adapters/Storage/Mongo/MongoTransform.js index 01c1aa1a8a..d5e8c468e0 100644 --- a/src/Adapters/Storage/Mongo/MongoTransform.js +++ b/src/Adapters/Storage/Mongo/MongoTransform.js @@ -1,5 +1,6 @@ import log from '../../../logger'; import _ from 'lodash'; +import StorageUtils from '../StorageUtils'; var mongodb = require('mongodb'); var Parse = require('parse/node').Parse; @@ -717,29 +718,26 @@ function transformConstraint(constraint, field) { } case '$in': - case '$nin': { - const arr = constraint[key]; - if (!(arr instanceof Array)) { - throw new Parse.Error(Parse.Error.INVALID_JSON, 'bad ' + key + ' value'); - } - answer[key] = _.flatMap(arr, value => { - return ((atom) => { - if (Array.isArray(atom)) { - return value.map(transformer); - } else { - return transformer(atom); - } - })(value); - }); - break; - } + case '$nin': case '$all': { const arr = constraint[key]; if (!(arr instanceof Array)) { throw new Parse.Error(Parse.Error.INVALID_JSON, 'bad ' + key + ' value'); } - answer[key] = arr.map(transformInteriorAtom); + if (key === '$all') { + answer[key] = arr.map(transformInteriorAtom); + } else { + answer[key] = _.flatMap(arr, value => { + return ((atom) => { + if (Array.isArray(atom)) { + return value.map(transformer); + } else { + return transformer(atom); + } + })(value); + }); + } break; } case '$regex': @@ -772,14 +770,7 @@ function transformConstraint(constraint, field) { '$search': search.$term } } - if (search.$language && typeof search.$language !== 'string') { - throw new Parse.Error( - Parse.Error.INVALID_JSON, - `bad $text: $language, should be string` - ); - } else if (search.$language) { - answer[key].$language = search.$language; - } + answer[key].$language = StorageUtils.getLanguageFromSearch(search); if (search.$caseSensitive && typeof search.$caseSensitive !== 'boolean') { throw new Parse.Error( Parse.Error.INVALID_JSON, @@ -1284,24 +1275,7 @@ var PolygonCoder = { coords[0][1] !== coords[coords.length - 1][1]) { coords.push(coords[0]); } - const unique = coords.filter((item, index, ar) => { - let foundIndex = -1; - for (let i = 0; i < ar.length; i += 1) { - const pt = ar[i]; - if (pt[0] === item[0] && - pt[1] === item[1]) { - foundIndex = i; - break; - } - } - return foundIndex === index; - }); - if (unique.length < 3) { - throw new Parse.Error( - Parse.Error.INTERNAL_SERVER_ERROR, - 'GeoJSON: Loop must have at least 3 different vertices' - ); - } + StorageUtils.verifyCoordinatesUnique(coords); return { type: 'Polygon', coordinates: [coords] }; }, diff --git a/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js b/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js index 246d5a6011..291d5f04ca 100644 --- a/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js +++ b/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js @@ -2,6 +2,7 @@ import { createClient } from './PostgresClient'; import Parse from 'parse/node'; import _ from 'lodash'; import sql from './sql'; +import StorageUtils from '../StorageUtils'; const PostgresRelationDoesNotExistError = '42P01'; const PostgresDuplicateRelationError = '42P07'; @@ -67,25 +68,6 @@ const transformValue = value => { return value; } -// Duplicate from then mongo adapter... -const emptyCLPS = Object.freeze({ - find: {}, - get: {}, - create: {}, - update: {}, - delete: {}, - addField: {}, -}); - -const defaultCLPS = Object.freeze({ - find: {'*': true}, - get: {'*': true}, - create: {'*': true}, - update: {'*': true}, - delete: {'*': true}, - addField: {'*': true}, -}); - const toParseSchema = (schema) => { if (schema.className === '_User') { delete schema.fields._hashed_password; @@ -94,9 +76,9 @@ const toParseSchema = (schema) => { delete schema.fields._wperm; delete schema.fields._rperm; } - let clps = defaultCLPS; + let clps = StorageUtils.getDefaultCLPs(); if (schema.classLevelPermissions) { - clps = {...emptyCLPS, ...schema.classLevelPermissions}; + clps = {...StorageUtils.getEmptyCLPs(), ...schema.classLevelPermissions}; } return { className: schema.className, @@ -376,7 +358,7 @@ const buildWhereClause = ({ schema, query, index }) => { if (fieldValue.$text) { const search = fieldValue.$text.$search; - let language = 'english'; + const language = StorageUtils.getLanguageFromSearch(search); if (typeof search !== 'object') { throw new Parse.Error( Parse.Error.INVALID_JSON, @@ -389,14 +371,6 @@ const buildWhereClause = ({ schema, query, index }) => { `bad $text: $term, should be string` ); } - if (search.$language && typeof search.$language !== 'string') { - throw new Parse.Error( - Parse.Error.INVALID_JSON, - `bad $text: $language, should be string` - ); - } else if (search.$language) { - language = search.$language; - } if (search.$caseSensitive && typeof search.$caseSensitive !== 'boolean') { throw new Parse.Error( Parse.Error.INVALID_JSON, @@ -1064,22 +1038,27 @@ export class PostgresStorageAdapter { updatePatterns.push(`$${index}:name = COALESCE($${index}:name, 0) + $${index + 1}`); values.push(fieldName, fieldValue.amount); index += 2; - } else if (fieldValue.__op === 'Add') { - updatePatterns.push(`$${index}:name = array_add(COALESCE($${index}:name, '[]'::jsonb), $${index + 1}::jsonb)`); + } else if ( + fieldValue.__op === 'Add' || + fieldValue.__op === 'Remove' || + fieldValue.__op === 'AddUnique' + ) { values.push(fieldName, JSON.stringify(fieldValue.objects)); + if(fieldValue.__op === 'Add') { + // Add + updatePatterns.push(`$${index}:name = array_add(COALESCE($${index}:name, '[]'::jsonb), $${index + 1}::jsonb)`); + } else if (fieldValue.__op === 'Remove') { + // Remove + updatePatterns.push(`$${index}:name = array_remove(COALESCE($${index}:name, '[]'::jsonb), $${index + 1}::jsonb)`); + } else { + // AddUnique + updatePatterns.push(`$${index}:name = array_add_unique(COALESCE($${index}:name, '[]'::jsonb), $${index + 1}::jsonb)`); + } index += 2; } else if (fieldValue.__op === 'Delete') { updatePatterns.push(`$${index}:name = $${index + 1}`) values.push(fieldName, null); index += 2; - } else if (fieldValue.__op === 'Remove') { - updatePatterns.push(`$${index}:name = array_remove(COALESCE($${index}:name, '[]'::jsonb), $${index + 1}::jsonb)`) - values.push(fieldName, JSON.stringify(fieldValue.objects)); - index += 2; - } else if (fieldValue.__op === 'AddUnique') { - updatePatterns.push(`$${index}:name = array_add_unique(COALESCE($${index}:name, '[]'::jsonb), $${index + 1}::jsonb)`); - values.push(fieldName, JSON.stringify(fieldValue.objects)); - index += 2; } else if (fieldName === 'updatedAt') { //TODO: stop special casing this. It should check for __type === 'Date' and use .iso updatePatterns.push(`$${index}:name = $${index + 1}`) values.push(fieldName, fieldValue); @@ -1203,6 +1182,17 @@ export class PostgresStorageAdapter { }); } + getShouldSortPattern(sort) { + const sorting = Object.keys(sort).map((key) => { + // Using $idx pattern gives: non-integer constant in ORDER BY + if (sort[key] === 1) { + return `"${key}" ASC`; + } + return `"${key}" DESC`; + }).join(','); + return sort !== undefined && Object.keys(sort).length > 0 ? `ORDER BY ${sorting}` : ''; + } + find(className, schema, query, { skip, limit, sort, keys }) { debug('find', className, query, {skip, limit, sort, keys }); const hasLimit = limit !== undefined; @@ -1223,14 +1213,7 @@ export class PostgresStorageAdapter { let sortPattern = ''; if (sort) { - const sorting = Object.keys(sort).map((key) => { - // Using $idx pattern gives: non-integer constant in ORDER BY - if (sort[key] === 1) { - return `"${key}" ASC`; - } - return `"${key}" DESC`; - }).join(','); - sortPattern = sort !== undefined && Object.keys(sort).length > 0 ? `ORDER BY ${sorting}` : ''; + sortPattern = this.getShouldSortPattern(sort); } if (where.sorts && Object.keys(where.sorts).length > 0) { sortPattern = `ORDER BY ${where.sorts.join(',')}`; @@ -1490,13 +1473,7 @@ export class PostgresStorageAdapter { } if (stage.$sort) { const sort = stage.$sort; - const sorting = Object.keys(sort).map((key) => { - if (sort[key] === 1) { - return `"${key}" ASC`; - } - return `"${key}" DESC`; - }).join(','); - sortPattern = sort !== undefined && Object.keys(sort).length > 0 ? `ORDER BY ${sorting}` : ''; + sortPattern = this.getShouldSortPattern(sort); } } @@ -1561,24 +1538,7 @@ function convertPolygonToSQL(polygon) { polygon[0][1] !== polygon[polygon.length - 1][1]) { polygon.push(polygon[0]); } - const unique = polygon.filter((item, index, ar) => { - let foundIndex = -1; - for (let i = 0; i < ar.length; i += 1) { - const pt = ar[i]; - if (pt[0] === item[0] && - pt[1] === item[1]) { - foundIndex = i; - break; - } - } - return foundIndex === index; - }); - if (unique.length < 3) { - throw new Parse.Error( - Parse.Error.INTERNAL_SERVER_ERROR, - 'GeoJSON: Loop must have at least 3 different vertices' - ); - } + StorageUtils.verifyCoordinatesUnique(polygon); const points = polygon.map((point) => { Parse.GeoPoint._validate(parseFloat(point[1]), parseFloat(point[0])); return `(${point[1]}, ${point[0]})`; diff --git a/src/Adapters/Storage/StorageUtils.js b/src/Adapters/Storage/StorageUtils.js new file mode 100644 index 0000000000..574fd37df8 --- /dev/null +++ b/src/Adapters/Storage/StorageUtils.js @@ -0,0 +1,69 @@ +// Various Storage utilities + +const Parse = require('parse/node').Parse; + +export class StorageUtils { + + static getEmptyCLPs() { + return Object.freeze({ + find: {}, + get: {}, + create: {}, + update: {}, + delete: {}, + addField: {}, + }); + } + + static getDefaultCLPs() { + return Object.freeze({ + find: {'*': true}, + get: {'*': true}, + create: {'*': true}, + update: {'*': true}, + delete: {'*': true}, + addField: {'*': true}, + }); + } + + /** + * Verifies unique coordinates + * + * @param {Object} coords Coords to verify uniqueness of + */ + static verifyCoordinatesUnique(coords) { + const unique = coords.filter((item, index, ar) => { + let foundIndex = -1; + for (let i = 0; i < ar.length; i += 1) { + const pt = ar[i]; + if (pt[0] === item[0] && + pt[1] === item[1]) { + foundIndex = i; + break; + } + } + return foundIndex === index; + }); + if (unique.length < 3) { + throw new Parse.Error( + Parse.Error.INTERNAL_SERVER_ERROR, + 'GeoJSON: Loop must have at least 3 different vertices' + ); + } + } + + static getLanguageFromSearch(search) { + if (search.$language && typeof search.$language !== 'string') { + throw new Parse.Error( + Parse.Error.INVALID_JSON, + `bad $text: $language, should be string` + ); + } else if (search.$language) { + return search.$language; + } + return 'english'; // default lang + } + +} + +export default StorageUtils; diff --git a/src/Controllers/DatabaseController.js b/src/Controllers/DatabaseController.js index a954cfb409..53a6919c59 100644 --- a/src/Controllers/DatabaseController.js +++ b/src/Controllers/DatabaseController.js @@ -493,11 +493,6 @@ const flattenUpdateOperatorsForCreate = object => { object[key] = object[key].amount; break; case 'Add': - if (!(object[key].objects instanceof Array)) { - throw new Parse.Error(Parse.Error.INVALID_JSON, 'objects to add must be an array'); - } - object[key] = object[key].objects; - break; case 'AddUnique': if (!(object[key].objects instanceof Array)) { throw new Parse.Error(Parse.Error.INVALID_JSON, 'objects to add must be an array'); @@ -724,6 +719,18 @@ DatabaseController.prototype.reduceRelationKeys = function(className, query, que } }; +// Makes sure we don't clobber existing shorthand $eq constraints on objectId. +DatabaseController.prototype.preserveObjectIdConstraint = function(query) { + if (!('objectId' in query)) { + query.objectId = {}; + } else if (typeof query.objectId === 'string') { + query.objectId = { + $eq: query.objectId + }; + } + return query; +}; + DatabaseController.prototype.addInObjectIdsIds = function(ids = null, query) { const idsFromString = typeof query.objectId === 'string' ? [query.objectId] : null; const idsFromEq = query.objectId && query.objectId['$eq'] ? [query.objectId['$eq']] : null; @@ -740,13 +747,7 @@ DatabaseController.prototype.addInObjectIdsIds = function(ids = null, query) { } // Need to make sure we don't clobber existing shorthand $eq constraints on objectId. - if (!('objectId' in query)) { - query.objectId = {}; - } else if (typeof query.objectId === 'string') { - query.objectId = { - $eq: query.objectId - }; - } + query = this.preserveObjectIdConstraint(query); query.objectId['$in'] = idsIntersection; return query; @@ -760,13 +761,7 @@ DatabaseController.prototype.addNotInObjectIdsIds = function(ids = [], query) { allIds = [...new Set(allIds)]; // Need to make sure we don't clobber existing shorthand $eq constraints on objectId. - if (!('objectId' in query)) { - query.objectId = {}; - } else if (typeof query.objectId === 'string') { - query.objectId = { - $eq: query.objectId - }; - } + query = this.preserveObjectIdConstraint(query); query.objectId['$nin'] = allIds; return query; diff --git a/src/LiveQuery/RequestSchema.js b/src/LiveQuery/RequestSchema.js index 05cfed3275..27aab68232 100644 --- a/src/LiveQuery/RequestSchema.js +++ b/src/LiveQuery/RequestSchema.js @@ -40,11 +40,11 @@ const connect = { "additionalProperties": false }; -const subscribe = { - 'title': 'Subscribe operation schema', +const base = { + 'title': 'TITLE', 'type': 'object', 'properties': { - 'op': 'subscribe', + 'op': 'OP', 'requestId': { 'type': 'number' }, @@ -78,43 +78,13 @@ const subscribe = { 'additionalProperties': false }; -const update = { - 'title': 'Update operation schema', - 'type': 'object', - 'properties': { - 'op': 'update', - 'requestId': { - 'type': 'number' - }, - 'query': { - 'title': 'Query field schema', - 'type': 'object', - 'properties': { - 'className': { - 'type': 'string' - }, - 'where': { - 'type': 'object' - }, - 'fields': { - "type": "array", - "items": { - "type": "string" - }, - "minItems": 1, - "uniqueItems": true - } - }, - 'required': ['where', 'className'], - 'additionalProperties': false - }, - 'sessionToken': { - 'type': 'string' - } - }, - 'required': ['op', 'requestId', 'query'], - 'additionalProperties': false -}; +const subscribe = base; +subscribe['title'] = 'Subscribe operation schema'; +subscribe['properties']['op'] = 'subscribe'; + +const update = base; +subscribe['title'] = 'Update operation schema'; +subscribe['properties']['op'] = 'update'; const unsubscribe = { 'title': 'Unsubscribe operation schema', diff --git a/src/RestQuery.js b/src/RestQuery.js index 8ddda22cba..4fb9380194 100644 --- a/src/RestQuery.js +++ b/src/RestQuery.js @@ -1,9 +1,9 @@ // An object that encapsulates everything we need to run a 'find' // operation, encoded in the REST API format. -var SchemaController = require('./Controllers/SchemaController'); var Parse = require('parse/node').Parse; const triggers = require('./triggers'); +import RestUtils from './RestUtils'; const AlwaysSelectedKeys = ['objectId', 'createdAt', 'updatedAt']; // restOptions can include: @@ -168,7 +168,7 @@ RestQuery.prototype.buildRestWhere = function() { }).then(() => { return this.redirectClassNameForKey(); }).then(() => { - return this.validateClientClassCreation(); + return RestUtils.validateClientClassCreation(this.config, this.auth, this.className); }).then(() => { return this.replaceSelect(); }).then(() => { @@ -210,33 +210,20 @@ RestQuery.prototype.redirectClassNameForKey = function() { }); }; -// Validates this operation against the allowClientClassCreation config. -RestQuery.prototype.validateClientClassCreation = function() { - if (this.config.allowClientClassCreation === false && !this.auth.isMaster - && SchemaController.systemClasses.indexOf(this.className) === -1) { - return this.config.database.loadSchema() - .then(schemaController => schemaController.hasClass(this.className)) - .then(hasClass => { - if (hasClass !== true) { - throw new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, - 'This user is not allowed to access ' + - 'non-existent class: ' + this.className); - } - }); - } else { - return Promise.resolve(); - } -}; - -function transformInQuery(inQueryObject, className, results) { - var values = []; - for (var result of results) { +function getQueryPointers(results, className) { + const values = []; + for (const result of results) { values.push({ __type: 'Pointer', className: className, objectId: result.objectId }); } + return values; +} + +function transformInQuery(inQueryObject, className, results) { + const values = getQueryPointers(results, className); delete inQueryObject['$inQuery']; if (Array.isArray(inQueryObject['$in'])) { inQueryObject['$in'] = inQueryObject['$in'].concat(values); @@ -282,14 +269,7 @@ RestQuery.prototype.replaceInQuery = function() { }; function transformNotInQuery(notInQueryObject, className, results) { - var values = []; - for (var result of results) { - values.push({ - __type: 'Pointer', - className: className, - objectId: result.objectId - }); - } + const values = getQueryPointers(results, className); delete notInQueryObject['$notInQuery']; if (Array.isArray(notInQueryObject['$nin'])) { notInQueryObject['$nin'] = notInQueryObject['$nin'].concat(values); diff --git a/src/RestUtils.js b/src/RestUtils.js new file mode 100644 index 0000000000..2d2e185eb0 --- /dev/null +++ b/src/RestUtils.js @@ -0,0 +1,42 @@ +// Various utilities for RestWrite & RestQuery + +const SchemaController = require('./Controllers/SchemaController'); +const Parse = require('parse/node').Parse; + +export class RestUtils { + // Validates this operation against the allowClientClassCreation config. + static validateClientClassCreation(config, auth, className) { + if (config.allowClientClassCreation === false && !auth.isMaster + && SchemaController.systemClasses.indexOf(className) === -1) { + return config.database.loadSchema() + .then(schemaController => schemaController.hasClass(className)) + .then(hasClass => { + if (hasClass !== true) { + throw new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, + 'This user is not allowed to access ' + + 'non-existent class: ' + className); + } + }); + } else { + return Promise.resolve(); + } + } + + // Cleans auth data from a user + static cleanUserAuthData(user) { + if (user.authData) { + Object.keys(user.authData).forEach((provider) => { + if (user.authData[provider] === null) { + delete user.authData[provider]; + } + }); + if (Object.keys(user.authData).length == 0) { + delete user.authData; + } + } + return user; + } + +} + +export default RestUtils; diff --git a/src/RestWrite.js b/src/RestWrite.js index 424284d5ba..573e43abe4 100644 --- a/src/RestWrite.js +++ b/src/RestWrite.js @@ -2,15 +2,14 @@ // that writes to the database. // This could be either a "create" or an "update". -var SchemaController = require('./Controllers/SchemaController'); var deepcopy = require('deepcopy'); - var Auth = require('./Auth'); var cryptoUtils = require('./cryptoUtils'); var passwordCrypto = require('./password'); var Parse = require('parse/node'); var triggers = require('./triggers'); var ClientSDK = require('./ClientSDK'); +import RestUtils from './RestUtils'; import RestQuery from './RestQuery'; import _ from 'lodash'; import logger from './logger'; @@ -64,7 +63,7 @@ RestWrite.prototype.execute = function() { return Promise.resolve().then(() => { return this.getUserAndRoleACL(); }).then(() => { - return this.validateClientClassCreation(); + return RestUtils.validateClientClassCreation(this.config, this.auth, this.className); }).then(() => { return this.handleInstallation(); }).then(() => { @@ -116,24 +115,6 @@ RestWrite.prototype.getUserAndRoleACL = function() { } }; -// Validates this operation against the allowClientClassCreation config. -RestWrite.prototype.validateClientClassCreation = function() { - if (this.config.allowClientClassCreation === false && !this.auth.isMaster - && SchemaController.systemClasses.indexOf(this.className) === -1) { - return this.config.database.loadSchema() - .then(schemaController => schemaController.hasClass(this.className)) - .then(hasClass => { - if (hasClass !== true) { - throw new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, - 'This user is not allowed to access ' + - 'non-existent class: ' + this.className); - } - }); - } else { - return Promise.resolve(); - } -}; - // Validates this operation against the schema. RestWrite.prototype.validateSchema = function() { return this.config.database.validateObject(this.className, this.data, this.query, this.runOptions); @@ -421,6 +402,25 @@ RestWrite.prototype.transformUser = function() { }); }; +// Returns any users with matching usernames +// but not equal to the current user id +RestWrite.prototype._findDuplicateUsernames = function() { + return this.config.database.find( + this.className, + {username: this.data.username, objectId: {'$ne': this.objectId()} }, + {limit: 1} + ) +}; + +// Returns any users with matching emails +RestWrite.prototype._findDuplicateEmails = function() { + return this.config.database.find( + this.className, + {email: this.data.email, objectId: {'$ne': this.objectId()}}, + {limit: 1} + ); +}; + RestWrite.prototype._validateUserName = function () { // Check for username uniqueness if (!this.data.username) { @@ -432,11 +432,7 @@ RestWrite.prototype._validateUserName = function () { } // We need to a find to check for duplicate username in case they are missing the unique index on usernames // TODO: Check if there is a unique index, and if so, skip this query. - return this.config.database.find( - this.className, - {username: this.data.username, objectId: {'$ne': this.objectId()}}, - {limit: 1} - ).then(results => { + return this._findDuplicateUsernames().then(results => { if (results.length > 0) { throw new Parse.Error(Parse.Error.USERNAME_TAKEN, 'Account already exists for this username.'); } @@ -453,11 +449,7 @@ RestWrite.prototype._validateEmail = function() { return Promise.reject(new Parse.Error(Parse.Error.INVALID_EMAIL_ADDRESS, 'Email address format is invalid.')); } // Same problem for email as above for username - return this.config.database.find( - this.className, - {email: this.data.email, objectId: {'$ne': this.objectId()}}, - {limit: 1} - ).then(results => { + return this._findDuplicateEmails().then(results => { if (results.length > 0) { throw new Parse.Error(Parse.Error.EMAIL_TAKEN, 'Account already exists for this email address.'); } @@ -512,37 +504,42 @@ RestWrite.prototype._validatePasswordRequirements = function() { return Promise.resolve(); }; +RestWrite.prototype._getUser = function() { + return this.config.database.find('_User', {objectId: this.objectId()}, {keys: ["_password_history", "_hashed_password"]}) + .then(results => { + if (results.length !== 1) { + throw undefined; + } + return results[0]; + }); +} + RestWrite.prototype._validatePasswordHistory = function() { // check whether password is repeating from specified history if (this.query && this.config.passwordPolicy.maxPasswordHistory) { - return this.config.database.find('_User', {objectId: this.objectId()}, {keys: ["_password_history", "_hashed_password"]}) - .then(results => { - if (results.length != 1) { - throw undefined; - } - const user = results[0]; - let oldPasswords = []; - if (user._password_history) - oldPasswords = _.take(user._password_history, this.config.passwordPolicy.maxPasswordHistory - 1); - oldPasswords.push(user.password); - const newPassword = this.data.password; - // compare the new password hash with all old password hashes - const promises = oldPasswords.map(function (hash) { - return passwordCrypto.compare(newPassword, hash).then((result) => { - if (result) // reject if there is a match - return Promise.reject("REPEAT_PASSWORD"); - return Promise.resolve(); - }) - }); - // wait for all comparisons to complete - return Promise.all(promises).then(() => { + return this._getUser().then(user => { + let oldPasswords = []; + if (user._password_history) + oldPasswords = _.take(user._password_history, this.config.passwordPolicy.maxPasswordHistory - 1); + oldPasswords.push(user.password); + const newPassword = this.data.password; + // compare the new password hash with all old password hashes + const promises = oldPasswords.map(function (hash) { + return passwordCrypto.compare(newPassword, hash).then((result) => { + if (result) // reject if there is a match + return Promise.reject("REPEAT_PASSWORD"); return Promise.resolve(); - }).catch(err => { - if (err === "REPEAT_PASSWORD") // a match was found - return Promise.reject(new Parse.Error(Parse.Error.VALIDATION_ERROR, `New password should not be the same as last ${this.config.passwordPolicy.maxPasswordHistory} passwords.`)); - throw err; - }); + }) }); + // wait for all comparisons to complete + return Promise.all(promises).then(() => { + return Promise.resolve(); + }).catch(err => { + if (err === "REPEAT_PASSWORD") // a match was found + return Promise.reject(new Parse.Error(Parse.Error.VALIDATION_ERROR, `New password should not be the same as last ${this.config.passwordPolicy.maxPasswordHistory} passwords.`)); + throw err; + }); + }); } return Promise.resolve(); }; @@ -712,6 +709,21 @@ RestWrite.prototype.handleSession = function() { } }; +RestWrite.prototype.cleanInstallations = function(query) { + if (this.data.appIdentifier) { + query['appIdentifier'] = this.data.appIdentifier; + } + this.config.database.destroy('_Installation', query) + .catch(err => { + if (err.code == Parse.Error.OBJECT_NOT_FOUND) { + // no deletions were made. Can be ignored. + return; + } + // rethrow the error + throw err; + }); +} + // Handles the _Installation class specialness. // Does nothing if this isn't an installation object. // If an installation is found, this can mutate this.query and turn a create @@ -867,18 +879,7 @@ RestWrite.prototype.handleInstallation = function() { '$ne': installationId } }; - if (this.data.appIdentifier) { - delQuery['appIdentifier'] = this.data.appIdentifier; - } - this.config.database.destroy('_Installation', delQuery) - .catch(err => { - if (err.code == Parse.Error.OBJECT_NOT_FOUND) { - // no deletions were made. Can be ignored. - return; - } - // rethrow the error - throw err; - }); + this.cleanInstallations(delQuery); return; } } else { @@ -925,18 +926,7 @@ RestWrite.prototype.handleInstallation = function() { // What to do here? can't really clean up everything... return idMatch.objectId; } - if (this.data.appIdentifier) { - delQuery['appIdentifier'] = this.data.appIdentifier; - } - this.config.database.destroy('_Installation', delQuery) - .catch(err => { - if (err.code == Parse.Error.OBJECT_NOT_FOUND) { - // no deletions were made. Can be ignored. - return; - } - // rethrow the error - throw err; - }); + this.cleanInstallations(delQuery); } // In non-merge scenarios, just return the installation match id return idMatch.objectId; @@ -1004,11 +994,7 @@ RestWrite.prototype.runDatabaseOperation = function() { let defer = Promise.resolve(); // if password history is enabled then save the current password to history if (this.className === '_User' && this.data._hashed_password && this.config.passwordPolicy && this.config.passwordPolicy.maxPasswordHistory) { - defer = this.config.database.find('_User', {objectId: this.objectId()}, {keys: ["_password_history", "_hashed_password"]}).then(results => { - if (results.length != 1) { - throw undefined; - } - const user = results[0]; + defer = this._getUser().then(user => { let oldPasswords = []; if (user._password_history) { oldPasswords = _.take(user._password_history, this.config.passwordPolicy.maxPasswordHistory); @@ -1069,27 +1055,17 @@ RestWrite.prototype.runDatabaseOperation = function() { // check whether it was username or email and return the appropriate error. // Fallback to the original method // TODO: See if we can later do this without additional queries by using named indexes. - return this.config.database.find( - this.className, - { username: this.data.username, objectId: {'$ne': this.objectId()} }, - { limit: 1 } - ) - .then(results => { - if (results.length > 0) { - throw new Parse.Error(Parse.Error.USERNAME_TAKEN, 'Account already exists for this username.'); - } - return this.config.database.find( - this.className, - { email: this.data.email, objectId: {'$ne': this.objectId()} }, - { limit: 1 } - ); - }) - .then(results => { - if (results.length > 0) { - throw new Parse.Error(Parse.Error.EMAIL_TAKEN, 'Account already exists for this email address.'); - } - throw new Parse.Error(Parse.Error.DUPLICATE_VALUE, 'A duplicate value for a field with unique values was provided'); - }); + return this._findDuplicateUsernames().then(results => { + if (results.length > 0) { + throw new Parse.Error(Parse.Error.USERNAME_TAKEN, 'Account already exists for this username.'); + } + return this._findDuplicateEmails(); + }).then(results => { + if (results.length > 0) { + throw new Parse.Error(Parse.Error.EMAIL_TAKEN, 'Account already exists for this email address.'); + } + throw new Parse.Error(Parse.Error.DUPLICATE_VALUE, 'A duplicate value for a field with unique values was provided'); + }); }) .then(response => { response.objectId = this.data.objectId; @@ -1195,19 +1171,11 @@ RestWrite.prototype.buildUpdatedObject = function (extraData) { return updatedObject; }; +// TODO: (montymxb) UsersRouter cleans authData from user in handleLogIn, this may be redundant RestWrite.prototype.cleanUserAuthData = function() { if (this.response && this.response.response && this.className === '_User') { const user = this.response.response; - if (user.authData) { - Object.keys(user.authData).forEach((provider) => { - if (user.authData[provider] === null) { - delete user.authData[provider]; - } - }); - if (Object.keys(user.authData).length == 0) { - delete user.authData; - } - } + RestUtils.cleanUserAuthData(user); } }; diff --git a/src/Routers/AggregateRouter.js b/src/Routers/AggregateRouter.js index 8f4e859b6d..fd06fca921 100644 --- a/src/Routers/AggregateRouter.js +++ b/src/Routers/AggregateRouter.js @@ -1,5 +1,4 @@ import ClassesRouter from './ClassesRouter'; -import rest from '../rest'; import * as middleware from '../middlewares'; import Parse from 'parse/node'; import UsersRouter from './UsersRouter'; @@ -63,17 +62,7 @@ export class AggregateRouter extends ClassesRouter { options.distinct = String(body.distinct); } options.pipeline = pipeline; - if (typeof body.where === 'string') { - body.where = JSON.parse(body.where); - } - return rest.find(req.config, req.auth, this.className(req), body.where, options, req.info.clientSDK).then((response) => { - for(const result of response.results) { - if(typeof result === 'object') { - UsersRouter.removeHiddenProperties(result); - } - } - return { response }; - }); + return this.runFind(req, body, options).then((response) => { return { response }; }); } mountRoutes() { diff --git a/src/Routers/AudiencesRouter.js b/src/Routers/AudiencesRouter.js index 3dbb94cf85..65328bfeb8 100644 --- a/src/Routers/AudiencesRouter.js +++ b/src/Routers/AudiencesRouter.js @@ -1,5 +1,4 @@ import ClassesRouter from './ClassesRouter'; -import rest from '../rest'; import * as middleware from '../middlewares'; export class AudiencesRouter extends ClassesRouter { @@ -9,18 +8,12 @@ export class AudiencesRouter extends ClassesRouter { } handleFind(req) { - const body = Object.assign(req.body, ClassesRouter.JSONFromQuery(req.query)); - const options = ClassesRouter.optionsFromBody(body); - - return rest.find(req.config, req.auth, '_Audience', body.where, options, req.info.clientSDK) - .then((response) => { - - response.results.forEach((item) => { - item.query = JSON.parse(item.query); - }); - - return {response: response}; + return ClassesRouter.handleFindForClass('_Audience', req).then((response) => { + response.results.forEach((item) => { + item.query = JSON.parse(item.query); }); + return {response: response}; + }); } handleGet(req) { diff --git a/src/Routers/ClassesRouter.js b/src/Routers/ClassesRouter.js index 6801f3bc1c..f049d36de8 100644 --- a/src/Routers/ClassesRouter.js +++ b/src/Routers/ClassesRouter.js @@ -12,6 +12,13 @@ export class ClassesRouter extends PromiseRouter { return req.params.className; } + runFind(req, body, options) { + if (typeof body.where === 'string') { + body.where = JSON.parse(body.where); + } + return rest.find(req.config, req.auth, this.className(req), body.where, options, req.info.clientSDK); + } + handleFind(req) { const body = Object.assign(req.body, ClassesRouter.JSONFromQuery(req.query)); const options = ClassesRouter.optionsFromBody(body); @@ -22,10 +29,7 @@ export class ClassesRouter extends PromiseRouter { if (body.redirectClassNameForKey) { options.redirectClassNameForKey = String(body.redirectClassNameForKey); } - if (typeof body.where === 'string') { - body.where = JSON.parse(body.where); - } - return rest.find(req.config, req.auth, this.className(req), body.where, options, req.info.clientSDK) + return this.runFind(req, body, options) .then((response) => { if (response && response.results) { for (const result of response.results) { @@ -138,6 +142,12 @@ export class ClassesRouter extends PromiseRouter { return options; } + static handleFindForClass(className, req) { + const body = Object.assign(req.body, ClassesRouter.JSONFromQuery(req.query)); + const options = ClassesRouter.optionsFromBody(body); + return rest.find(req.config, req.auth, className, body.where, options, req.info.clientSDK); + } + mountRoutes() { this.route('GET', '/classes/:className', (req) => { return this.handleFind(req); }); this.route('GET', '/classes/:className/:objectId', (req) => { return this.handleGet(req); }); diff --git a/src/Routers/InstallationsRouter.js b/src/Routers/InstallationsRouter.js index 90ab113eb6..1d801ea6cf 100644 --- a/src/Routers/InstallationsRouter.js +++ b/src/Routers/InstallationsRouter.js @@ -1,7 +1,6 @@ // InstallationsRouter.js import ClassesRouter from './ClassesRouter'; -import rest from '../rest'; export class InstallationsRouter extends ClassesRouter { className() { @@ -9,13 +8,9 @@ export class InstallationsRouter extends ClassesRouter { } handleFind(req) { - const body = Object.assign(req.body, ClassesRouter.JSONFromQuery(req.query)); - const options = ClassesRouter.optionsFromBody(body); - return rest.find(req.config, req.auth, - '_Installation', body.where, options, req.info.clientSDK) - .then((response) => { - return {response: response}; - }); + return ClassesRouter.handleFindForClass('_Installation', req).then((response) => { + return {response: response}; + }); } mountRoutes() { diff --git a/src/Routers/PublicAPIRouter.js b/src/Routers/PublicAPIRouter.js index a126423cb0..096a689bf8 100644 --- a/src/Routers/PublicAPIRouter.js +++ b/src/Routers/PublicAPIRouter.js @@ -98,17 +98,28 @@ export class PublicAPIRouter extends PromiseRouter { }); } - requestResetPassword(req) { - - const config = req.config; - + /** + * Validates config + * + * @param {Object} config Config to validate + */ + validateConfig(config) { if(!config){ - this.invalidRequest(); + return this.invalidRequest(); } - if (!config.publicServerURL) { return this.missingPublicServerURL(); } + } + + requestResetPassword(req) { + + const config = req.config; + + const response = this.validateConfig(config); + if(response) { + return response; + } const { username, token } = req.query; @@ -131,12 +142,9 @@ export class PublicAPIRouter extends PromiseRouter { const config = req.config; - if(!config){ - this.invalidRequest(); - } - - if (!config.publicServerURL) { - return this.missingPublicServerURL(); + const response = this.validateConfig(config); + if(response) { + return response; } const { diff --git a/src/Routers/SessionsRouter.js b/src/Routers/SessionsRouter.js index ed9b3830f7..fe53de486c 100644 --- a/src/Routers/SessionsRouter.js +++ b/src/Routers/SessionsRouter.js @@ -12,13 +12,29 @@ export class SessionsRouter extends ClassesRouter { return '_Session'; } + /** + * Get the current session from the given request + * + * @param {Object} req Request to get session for + */ + static getCurrentSession(req) { + return rest.find( + req.config, + Auth.master(req.config), + '_Session', + { sessionToken: req.info.sessionToken }, + undefined, + req.info.clientSDK + ); + } + handleMe(req) { // TODO: Verify correct behavior if (!req.info || !req.info.sessionToken) { throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, 'Session token required.'); } - return rest.find(req.config, Auth.master(req.config), '_Session', { sessionToken: req.info.sessionToken }, undefined, req.info.clientSDK) + return SessionsRouter.getCurrentSession(req) .then((response) => { if (!response.results || response.results.length == 0) { throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, diff --git a/src/Routers/UsersRouter.js b/src/Routers/UsersRouter.js index 0cdba7b1e0..9b6e310148 100644 --- a/src/Routers/UsersRouter.js +++ b/src/Routers/UsersRouter.js @@ -8,6 +8,8 @@ import rest from '../rest'; import Auth from '../Auth'; import passwordCrypto from '../password'; import RestWrite from '../RestWrite'; +import SessionsRouter from './SessionsRouter'; +import RestUtils from "../RestUtils"; const cryptoUtils = require('../cryptoUtils'); export class UsersRouter extends ClassesRouter { @@ -138,16 +140,7 @@ export class UsersRouter extends ClassesRouter { // Sometimes the authData still has null on that keys // https://github.com/parse-community/parse-server/issues/935 - if (user.authData) { - Object.keys(user.authData).forEach((provider) => { - if (user.authData[provider] === null) { - delete user.authData[provider]; - } - }); - if (Object.keys(user.authData).length == 0) { - delete user.authData; - } - } + user = RestUtils.cleanUserAuthData(user); req.config.filesController.expandFilesInObject(req.config, user); @@ -181,9 +174,7 @@ export class UsersRouter extends ClassesRouter { handleLogOut(req) { const success = {response: {}}; if (req.info && req.info.sessionToken) { - return rest.find(req.config, Auth.master(req.config), '_Session', - { sessionToken: req.info.sessionToken }, undefined, req.info.clientSDK - ).then((records) => { + return SessionsRouter.getCurrentSession(req).then((records) => { if (records.results && records.results.length) { return rest.del(req.config, Auth.master(req.config), '_Session', records.results[0].objectId @@ -215,7 +206,7 @@ export class UsersRouter extends ClassesRouter { } } - handleResetRequest(req) { + _verifyRequest(req) { this._throwOnBadEmailConfig(req); const { email } = req.body; @@ -225,6 +216,11 @@ export class UsersRouter extends ClassesRouter { if (typeof email !== 'string') { throw new Parse.Error(Parse.Error.INVALID_EMAIL_ADDRESS, 'you must provide a valid email string'); } + } + + handleResetRequest(req) { + const { email } = req.body; + this._verifyRequest(req); const userController = req.config.userController; return userController.sendPasswordResetEmail(email).then(() => { return Promise.resolve({ @@ -240,16 +236,8 @@ export class UsersRouter extends ClassesRouter { } handleVerificationEmailRequest(req) { - this._throwOnBadEmailConfig(req); - const { email } = req.body; - if (!email) { - throw new Parse.Error(Parse.Error.EMAIL_MISSING, 'you must provide an email'); - } - if (typeof email !== 'string') { - throw new Parse.Error(Parse.Error.INVALID_EMAIL_ADDRESS, 'you must provide a valid email string'); - } - + this._verifyRequest(req); return req.config.database.find('_User', { email: email }).then((results) => { if (!results.length || results.length < 1) { throw new Parse.Error(Parse.Error.EMAIL_NOT_FOUND, `No user found with email ${email}`); diff --git a/src/rest.js b/src/rest.js index b428f43cb8..8e91dd6ba1 100644 --- a/src/rest.js +++ b/src/rest.js @@ -24,10 +24,8 @@ function checkLiveQuery(className, config) { return config.liveQueryController && config.liveQueryController.hasLiveQuery(className) } -// Returns a promise for an object with optional keys 'results' and 'count'. -function find(config, auth, className, restWhere, restOptions, clientSDK) { - enforceRoleSecurity('find', className, auth); - return triggers.maybeRunQueryTrigger(triggers.Types.beforeFind, className, restWhere, restOptions, config, auth).then((result) => { +function runTriggerAndQuery(className, restWhere, restOptions, config, auth, clientSDK, isGet = false) { + return triggers.maybeRunQueryTrigger(triggers.Types.beforeFind, className, restWhere, restOptions, config, auth, isGet).then((result) => { restWhere = result.restWhere || restWhere; restOptions = result.restOptions || restOptions; const query = new RestQuery(config, auth, className, restWhere, restOptions, clientSDK); @@ -35,16 +33,17 @@ function find(config, auth, className, restWhere, restOptions, clientSDK) { }); } +// Returns a promise for an object with optional keys 'results' and 'count'. +function find(config, auth, className, restWhere, restOptions, clientSDK) { + enforceRoleSecurity('find', className, auth); + return runTriggerAndQuery(className, restWhere, restOptions, config, auth, clientSDK); +} + // get is just like find but only queries an objectId. const get = (config, auth, className, objectId, restOptions, clientSDK) => { - var restWhere = { objectId }; + const restWhere = { objectId }; enforceRoleSecurity('get', className, auth); - return triggers.maybeRunQueryTrigger(triggers.Types.beforeFind, className, restWhere, restOptions, config, auth, true).then((result) => { - restWhere = result.restWhere || restWhere; - restOptions = result.restOptions || restOptions; - const query = new RestQuery(config, auth, className, restWhere, restOptions, clientSDK); - return query.execute(); - }); + return runTriggerAndQuery(className, restWhere, restOptions, config, auth, clientSDK, true); } // Returns a promise that doesn't resolve to any useful value. diff --git a/src/vendor/mongodbUrl.js b/src/vendor/mongodbUrl.js index 4e3689f0c3..d6ee9b1d96 100644 --- a/src/vendor/mongodbUrl.js +++ b/src/vendor/mongodbUrl.js @@ -645,6 +645,15 @@ function urlResolveObject(source, relative) { return urlParse(source, false, true).resolveObject(relative); } +//to support request.http +function supportRequestHTTP(result) { + if (result.pathname !== null || result.search !== null) { + result.path = (result.pathname ? result.pathname : '') + + (result.search ? result.search : ''); + } + return result; +} + /* istanbul ignore next: improve coverage */ Url.prototype.resolveObject = function(relative) { if (typeof relative === 'string') { @@ -814,10 +823,7 @@ Url.prototype.resolveObject = function(relative) { result.search = relative.search; result.query = relative.query; //to support http.request - if (result.pathname !== null || result.search !== null) { - result.path = (result.pathname ? result.pathname : '') + - (result.search ? result.search : ''); - } + result = supportRequestHTTP(result); result.href = result.format(); return result; } @@ -908,10 +914,7 @@ Url.prototype.resolveObject = function(relative) { } //to support request.http - if (result.pathname !== null || result.search !== null) { - result.path = (result.pathname ? result.pathname : '') + - (result.search ? result.search : ''); - } + result = supportRequestHTTP(result); result.auth = relative.auth || result.auth; result.slashes = result.slashes || relative.slashes; result.href = result.format();