|
|
@@ -106,5 +106,170 @@ proto.getQuery = function getQuery() {
|
|
|
* that have had their age field removed.
|
|
|
*/
|
|
|
proto.redactSafePortion = function redactSafePortion() {
|
|
|
- throw new Error("not implemented");
|
|
|
+ var self = this;
|
|
|
+
|
|
|
+ // This block contains the functions that make up the implementation of
|
|
|
+ // DocumentSourceMatch::redactSafePortion(). They will only be called after
|
|
|
+ // the Match expression has been successfully parsed so they can assume that
|
|
|
+ // input is well formed.
|
|
|
+
|
|
|
+ var isAllDigits = function(n) {
|
|
|
+ return typeof n == 'number' && !isNaN(n - n);
|
|
|
+ };
|
|
|
+
|
|
|
+ var isFieldnameRedactSafe = function isFieldnameRedactSafe(field) {
|
|
|
+ var dotPos = field.indexOf('.');
|
|
|
+ if (dotPos === -1)
|
|
|
+ return !isAllDigits(field);
|
|
|
+
|
|
|
+ var part = field.slice(0, dotPos),
|
|
|
+ rest = field.slice(dotPos+1, field.length);
|
|
|
+
|
|
|
+ return !isAllDigits(part) && isFieldnameRedactSafe(rest);
|
|
|
+ };
|
|
|
+
|
|
|
+ // Returns the redact-safe portion of an "inner" match expression. This is the layer like
|
|
|
+ // {$gt: 5} which does not include the field name. Returns an empty document if none of the
|
|
|
+ // expression can safely be promoted in front of a $redact.
|
|
|
+ var redactSavePortionDollarOps = function redactSafePortionDollarOps(expr) {
|
|
|
+ var output = {},
|
|
|
+ elem,i,j,k;
|
|
|
+
|
|
|
+ var keys = Object.keys(expr);
|
|
|
+ for (i = 0; i < keys.length; i++) {
|
|
|
+ var field = keys[i],
|
|
|
+ value = expr[field];
|
|
|
+
|
|
|
+ if (field[0] !== '$')
|
|
|
+ continue;
|
|
|
+
|
|
|
+ // Ripped the case apart and did not implement this painful thing:
|
|
|
+ // https://github.com/mongodb/mongo/blob/r2.5.4/src/mongo/db/jsobj.cpp#L286
|
|
|
+ // Somebody should be taken to task for that work of art.
|
|
|
+ if (field === '$type' || field === '$regex' || field === '$options' || field === '$mod') {
|
|
|
+ output[field] = value;
|
|
|
+ } else if (field === '$lte' || field === '$gte' || field === '$lt' || field === '$gt') {
|
|
|
+ if (isTypeRedactSaveInComparison(field))
|
|
|
+ output[field] = value;
|
|
|
+ } else if (field === '$in') {
|
|
|
+ // TODO: value/elem/field/etc may be mixed up and wrong here
|
|
|
+ var allOk = true;
|
|
|
+ for (j = 0; j < Object.keys(value); j++) {
|
|
|
+ elem = Object.keys(value)[j];
|
|
|
+ if (!isTypeRedactSaveInComparison(elem)) {
|
|
|
+ allOk = false;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (allOk) {
|
|
|
+ output[field] = value;
|
|
|
+ }
|
|
|
+ break;
|
|
|
+ } else if (field === '$all') {
|
|
|
+ // TODO: value/elem/field/etc may be mixed up and wrong here
|
|
|
+ var matches = [];
|
|
|
+ for (j = 0; j < field.length; j++) {
|
|
|
+ elem = Object.keys(value)[j];
|
|
|
+ if (isTypeRedactSaveInComparison(elem))
|
|
|
+ matches.push(value[elem]);
|
|
|
+ }
|
|
|
+ if (matches.length)
|
|
|
+ output[field] = matches;
|
|
|
+
|
|
|
+ } else if (field === '$elemMatch') {
|
|
|
+ var subIn = field,
|
|
|
+ subOut;
|
|
|
+
|
|
|
+ if (subIn[0] === '$')
|
|
|
+ subOut = redactSafePortionDollarOps(subIn);
|
|
|
+ else
|
|
|
+ subOut = redactSafePortionTopLevel(subIn);
|
|
|
+
|
|
|
+ if (subOut)
|
|
|
+ output[field] = subOut;
|
|
|
+
|
|
|
+ break;
|
|
|
+ } else {
|
|
|
+ // never allowed:
|
|
|
+ // equality, maxDist, near, ne, opSize, nin, exists, within, geoIntersects
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return output;
|
|
|
+ };
|
|
|
+
|
|
|
+ var isTypeRedactSaveInComparison = function isTypeRedactSaveInComparison(type) {
|
|
|
+ if (type instanceof Array || type instanceof Object || type === null || type === undefined)
|
|
|
+ return false;
|
|
|
+ return true;
|
|
|
+ };
|
|
|
+
|
|
|
+ // Returns the redact-safe portion of an "outer" match expression. This is the layer like
|
|
|
+ // {fieldName: {...}} which does include the field name. Returns an empty document if none of
|
|
|
+ // the expression can safely be promoted in front of a $redact.
|
|
|
+ var redactSafePortionTopLevel = function(topQuery) {
|
|
|
+ var output = {},
|
|
|
+ okClauses = [],
|
|
|
+ keys = Object.keys(query),
|
|
|
+ j, elm, clause;
|
|
|
+
|
|
|
+ for (var i = 0; i < keys.length; i++) {
|
|
|
+ var field = keys[i],
|
|
|
+ query = topQuery[field];
|
|
|
+
|
|
|
+ if (field.length && field[0] === '$') {
|
|
|
+ if (field === '$or') {
|
|
|
+ okClauses = [];
|
|
|
+ for (j = 0; j < Object.keys(field).length; j++) {
|
|
|
+ elm = field[Object.keys(field)[j]];
|
|
|
+ clause = redactSafePortionTopLevel(elm);
|
|
|
+
|
|
|
+ if (!clause) {
|
|
|
+ okClauses = [];
|
|
|
+ break;
|
|
|
+ }
|
|
|
+
|
|
|
+ okClauses.push(clause);
|
|
|
+ }
|
|
|
+
|
|
|
+ if (okClauses) {
|
|
|
+ output.$or = okClauses;
|
|
|
+ }
|
|
|
+ } else if (field === '$and') {
|
|
|
+ okClauses = [];
|
|
|
+ for (j = 0; j < Object.keys(field).length; j++) {
|
|
|
+ elm = field[Object.keys(field)[j]];
|
|
|
+ clause = redactSafePortionTopLevel(elm);
|
|
|
+
|
|
|
+ if (clause)
|
|
|
+ okClauses.push(clause);
|
|
|
+ }
|
|
|
+
|
|
|
+ if (okClauses.length)
|
|
|
+ output.$and = okClauses;
|
|
|
+ }
|
|
|
+
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!isFieldnameRedactSafe(field))
|
|
|
+ continue;
|
|
|
+
|
|
|
+ if (field instanceof Array || !field) {
|
|
|
+ continue;
|
|
|
+ } else {
|
|
|
+ // subobjects
|
|
|
+ var sub = redactSavePortionDollarOps(field);
|
|
|
+ if (sub)
|
|
|
+ output[field] = sub;
|
|
|
+
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return output;
|
|
|
+ };
|
|
|
+
|
|
|
+ return redactSafePortionTopLevel(this.getQuery());
|
|
|
};
|