fix(api-listings-pagination): ZMS-225 Encode page in next and previous cursor (#818)

* implement a mongopaging wrapper to encode page in the cursor

* update mongopaging validation schema to properly use base64url encoding

* list messages endpoint use embedded page in cursor instead of explicit user-provided

* addresses, auth, certs for listing endpoints use mongopaging wrapper

* dkim, domainaliases, filters - listing endpoints use mongopaging wrapper

* messages, storage, users, webhooks - listing endpoints use mongopaging wrapper

* mongopaging wrapper - if page negative due to externally crafter cursor, default to page 1
This commit is contained in:
NickOvt 2025-05-27 15:39:04 +03:00 committed by GitHub
parent 2f6591170e
commit 905b463a63
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 234 additions and 192 deletions

View file

@ -2,13 +2,12 @@
const config = require('wild-config');
const Joi = require('joi');
const MongoPaging = require('mongo-cursor-pagination');
const ObjectId = require('mongodb').ObjectId;
const tools = require('../tools');
const consts = require('../consts');
const roles = require('../roles');
const libmime = require('libmime');
const { nextPageCursorSchema, previousPageCursorSchema, pageNrSchema, sessSchema, sessIPSchema, booleanSchema, metaDataSchema } = require('../schemas');
const { nextPageCursorSchema, previousPageCursorSchema, sessSchema, sessIPSchema, booleanSchema, metaDataSchema } = require('../schemas');
const log = require('npmlog');
const isemail = require('isemail');
const {
@ -29,6 +28,7 @@ const {
} = require('../schemas/response/addresses-schemas');
const { userId, addressEmail, addressId } = require('../schemas/request/general-schemas');
const { Autoreply } = require('../schemas/request/addresses-schemas');
const { mongopagingFindWrapper } = require('../mongopaging-find-wrapper');
module.exports = (db, server, userHandler, settingsHandler) => {
server.get(
@ -53,7 +53,6 @@ module.exports = (db, server, userHandler, settingsHandler) => {
limit: Joi.number().default(20).min(1).max(250).description('How many records to return'),
next: nextPageCursorSchema,
previous: previousPageCursorSchema,
page: pageNrSchema,
sess: sessSchema,
ip: sessIPSchema
},
@ -122,7 +121,6 @@ module.exports = (db, server, userHandler, settingsHandler) => {
let query = result.value.query;
let forward = result.value.forward;
let limit = result.value.limit;
let page = result.value.page;
let pageNext = result.value.next;
let pagePrevious = result.value.previous;
@ -216,13 +214,14 @@ module.exports = (db, server, userHandler, settingsHandler) => {
if (pageNext) {
opts.next = pageNext;
} else if ((!page || page > 1) && pagePrevious) {
}
if (pagePrevious) {
opts.previous = pagePrevious;
}
let listing;
let listingWrapper;
try {
listing = await MongoPaging.find(db.users.collection('addresses'), opts);
listingWrapper = await mongopagingFindWrapper(db.users.collection('addresses'), opts);
} catch (err) {
res.status(500);
return res.json({
@ -231,18 +230,14 @@ module.exports = (db, server, userHandler, settingsHandler) => {
});
}
if (!listing.hasPrevious) {
page = 1;
}
let response = {
success: true,
query,
total,
page,
previousCursor: listing.hasPrevious ? listing.previous : false,
nextCursor: listing.hasNext ? listing.next : false,
results: (listing.results || []).map(addressData => {
page: listingWrapper.page,
previousCursor: listingWrapper.previousCursor,
nextCursor: listingWrapper.nextCursor,
results: (listingWrapper.listing.results || []).map(addressData => {
let values = {
id: addressData._id.toString(),
name: addressData.name || undefined,

View file

@ -1,13 +1,13 @@
'use strict';
const Joi = require('joi');
const MongoPaging = require('mongo-cursor-pagination');
const ObjectId = require('mongodb').ObjectId;
const tools = require('../tools');
const roles = require('../roles');
const { nextPageCursorSchema, previousPageCursorSchema, pageNrSchema, sessSchema, sessIPSchema, booleanSchema, usernameSchema } = require('../schemas');
const { nextPageCursorSchema, previousPageCursorSchema, sessSchema, sessIPSchema, booleanSchema, usernameSchema } = require('../schemas');
const { successRes } = require('../schemas/response/general-schemas');
const { userId } = require('../schemas/request/general-schemas');
const { mongopagingFindWrapper } = require('../mongopaging-find-wrapper');
module.exports = (db, server, userHandler) => {
server.post(
@ -327,7 +327,6 @@ module.exports = (db, server, userHandler) => {
limit: Joi.number().default(20).min(1).max(250).description('How many records to return'),
next: nextPageCursorSchema,
previous: previousPageCursorSchema,
page: pageNrSchema,
filterip: sessIPSchema.description('Limit listing only to values with specific IP address'),
sess: sessSchema,
@ -409,7 +408,6 @@ module.exports = (db, server, userHandler) => {
let action = result.value.action;
let ip = result.value.filterIp;
let page = result.value.page;
let pageNext = result.value.next;
let pagePrevious = result.value.previous;
@ -456,13 +454,14 @@ module.exports = (db, server, userHandler) => {
if (pageNext) {
opts.next = pageNext;
} else if ((!page || page > 1) && pagePrevious) {
}
if (pagePrevious) {
opts.previous = pagePrevious;
}
let listing;
let listingWrapper;
try {
listing = await MongoPaging.find(db.users.collection('authlog'), opts);
listingWrapper = await mongopagingFindWrapper(db.users.collection('authlog'), opts);
} catch (err) {
res.status(500);
return res.json({
@ -471,18 +470,14 @@ module.exports = (db, server, userHandler) => {
});
}
if (!listing.hasPrevious) {
page = 1;
}
let response = {
success: true,
action,
total,
page,
previousCursor: listing.hasPrevious ? listing.previous : false,
nextCursor: listing.hasNext ? listing.next : false,
results: (listing.results || []).map(resultData => {
page: listingWrapper.page,
previousCursor: listingWrapper.previousCursor,
nextCursor: listingWrapper.nextCursor,
results: (listingWrapper.listing.results || []).map(resultData => {
let response = {
id: (resultData._id || '').toString()
};

View file

@ -2,14 +2,14 @@
const config = require('wild-config');
const Joi = require('joi');
const MongoPaging = require('mongo-cursor-pagination');
const ObjectId = require('mongodb').ObjectId;
const CertHandler = require('../cert-handler');
const TaskHandler = require('../task-handler');
const tools = require('../tools');
const roles = require('../roles');
const { nextPageCursorSchema, previousPageCursorSchema, pageNrSchema, sessSchema, sessIPSchema, booleanSchema } = require('../schemas');
const { nextPageCursorSchema, previousPageCursorSchema, sessSchema, sessIPSchema, booleanSchema } = require('../schemas');
const { successRes, totalRes, pageRes, previousCursorRes, nextCursorRes } = require('../schemas/response/general-schemas');
const { mongopagingFindWrapper } = require('../mongopaging-find-wrapper');
const certificateSchema = Joi.string()
.empty('')
@ -49,7 +49,6 @@ module.exports = (db, server) => {
limit: Joi.number().default(20).min(1).max(250).description('How many records to return'),
next: nextPageCursorSchema,
previous: previousPageCursorSchema,
page: pageNrSchema,
sess: sessSchema,
ip: sessIPSchema
},
@ -131,7 +130,6 @@ module.exports = (db, server) => {
let query = result.value.query;
let altNames = result.value.altNames;
let limit = result.value.limit;
let page = result.value.page;
let pageNext = result.value.next;
let pagePrevious = result.value.previous;
@ -174,13 +172,14 @@ module.exports = (db, server) => {
if (pageNext) {
opts.next = pageNext;
} else if ((!page || page > 1) && pagePrevious) {
}
if (pagePrevious) {
opts.previous = pagePrevious;
}
let listing;
let listingWrapper;
try {
listing = await MongoPaging.find(db.database.collection('certs'), opts);
listingWrapper = await mongopagingFindWrapper(db.database.collection('certs'), opts);
} catch (err) {
res.status(500);
return res.json({
@ -189,18 +188,14 @@ module.exports = (db, server) => {
});
}
if (!listing.hasPrevious) {
page = 1;
}
let response = {
success: true,
query,
total,
page,
previousCursor: listing.hasPrevious ? listing.previous : false,
nextCursor: listing.hasNext ? listing.next : false,
results: (listing.results || []).map(certData => ({
page: listingWrapper.page,
previousCursor: listingWrapper.previousCursor,
nextCursor: listingWrapper.nextCursor,
results: (listingWrapper.listing.results || []).map(certData => ({
id: certData._id.toString(),
servername: certData.servername,
description: certData.description,

View file

@ -2,13 +2,13 @@
const config = require('wild-config');
const Joi = require('joi');
const MongoPaging = require('mongo-cursor-pagination');
const ObjectId = require('mongodb').ObjectId;
const DkimHandler = require('../dkim-handler');
const tools = require('../tools');
const roles = require('../roles');
const { nextPageCursorSchema, previousPageCursorSchema, pageNrSchema, sessSchema, sessIPSchema } = require('../schemas');
const { nextPageCursorSchema, previousPageCursorSchema, sessSchema, sessIPSchema } = require('../schemas');
const { successRes, totalRes, pageRes, previousCursorRes, nextCursorRes } = require('../schemas/response/general-schemas');
const { mongopagingFindWrapper } = require('../mongopaging-find-wrapper');
module.exports = (db, server) => {
const dkimHandler = new DkimHandler({
@ -31,7 +31,6 @@ module.exports = (db, server) => {
limit: Joi.number().default(20).min(1).max(250).description('How many records to return'),
next: nextPageCursorSchema,
previous: previousPageCursorSchema,
page: pageNrSchema,
sess: sessSchema,
ip: sessIPSchema
},
@ -97,7 +96,6 @@ module.exports = (db, server) => {
let query = result.value.query;
let limit = result.value.limit;
let page = result.value.page;
let pageNext = result.value.next;
let pagePrevious = result.value.previous;
@ -121,13 +119,14 @@ module.exports = (db, server) => {
if (pageNext) {
opts.next = pageNext;
} else if ((!page || page > 1) && pagePrevious) {
}
if (pagePrevious) {
opts.previous = pagePrevious;
}
let listing;
let listingWrapper;
try {
listing = await MongoPaging.find(db.database.collection('dkim'), opts);
listingWrapper = await mongopagingFindWrapper(db.database.collection('dkim'), opts);
} catch (err) {
res.status(500);
return res.json({
@ -136,18 +135,14 @@ module.exports = (db, server) => {
});
}
if (!listing.hasPrevious) {
page = 1;
}
let response = {
success: true,
query,
total,
page,
previousCursor: listing.hasPrevious ? listing.previous : false,
nextCursor: listing.hasNext ? listing.next : false,
results: (listing.results || []).map(dkimData => ({
page: listingWrapper.page,
previousCursor: listingWrapper.previousCursor,
nextCursor: listingWrapper.nextCursor,
results: (listingWrapper.listing.results || []).map(dkimData => ({
id: dkimData._id.toString(),
domain: dkimData.domain,
selector: dkimData.selector,

View file

@ -1,13 +1,13 @@
'use strict';
const Joi = require('joi');
const MongoPaging = require('mongo-cursor-pagination');
const ObjectId = require('mongodb').ObjectId;
const tools = require('../tools');
const roles = require('../roles');
const { nextPageCursorSchema, previousPageCursorSchema, pageNrSchema, sessSchema, sessIPSchema } = require('../schemas');
const { nextPageCursorSchema, previousPageCursorSchema, sessSchema, sessIPSchema } = require('../schemas');
const { publish, DOMAINALIAS_CREATED, DOMAINALIAS_DELETED } = require('../events');
const { successRes, totalRes, pageRes, previousCursorRes, nextCursorRes } = require('../schemas/response/general-schemas');
const { mongopagingFindWrapper } = require('../mongopaging-find-wrapper');
module.exports = (db, server) => {
server.get(
@ -24,7 +24,6 @@ module.exports = (db, server) => {
limit: Joi.number().default(20).min(1).max(250).description('How many records to return'),
next: nextPageCursorSchema,
previous: previousPageCursorSchema,
page: pageNrSchema,
sess: sessSchema,
ip: sessIPSchema
},
@ -83,7 +82,6 @@ module.exports = (db, server) => {
let query = result.value.query;
let limit = result.value.limit;
let page = result.value.page;
let pageNext = result.value.next;
let pagePrevious = result.value.previous;
@ -128,13 +126,14 @@ module.exports = (db, server) => {
if (pageNext) {
opts.next = pageNext;
} else if ((!page || page > 1) && pagePrevious) {
}
if (pagePrevious) {
opts.previous = pagePrevious;
}
let listing;
let listingWrapper;
try {
listing = await MongoPaging.find(db.users.collection('domainaliases'), opts);
listingWrapper = await mongopagingFindWrapper(db.users.collection('domainaliases'), opts);
} catch (err) {
res.status(500);
return res.json({
@ -143,18 +142,14 @@ module.exports = (db, server) => {
});
}
if (!listing.hasPrevious) {
page = 1;
}
let response = {
success: true,
query,
total,
page,
previousCursor: listing.hasPrevious ? listing.previous : false,
nextCursor: listing.hasNext ? listing.next : false,
results: (listing.results || []).map(domainData => ({
page: listingWrapper.page,
previousCursor: listingWrapper.previousCursor,
nextCursor: listingWrapper.nextCursor,
results: (listingWrapper.listing.results || []).map(domainData => ({
id: domainData._id.toString(),
alias: domainData.alias,
domain: domainData.domain

View file

@ -3,16 +3,16 @@
const log = require('npmlog');
const Joi = require('joi');
const ObjectId = require('mongodb').ObjectId;
const MongoPaging = require('mongo-cursor-pagination');
const urllib = require('url');
const tools = require('../tools');
const roles = require('../roles');
const { nextPageCursorSchema, previousPageCursorSchema, pageNrSchema, sessSchema, sessIPSchema, booleanSchema, metaDataSchema } = require('../schemas');
const { nextPageCursorSchema, previousPageCursorSchema, sessSchema, sessIPSchema, booleanSchema, metaDataSchema } = require('../schemas');
const { publish, FILTER_DELETED, FILTER_CREATED, FORWARD_ADDED } = require('../events');
const { successRes, totalRes, previousCursorRes, nextCursorRes } = require('../schemas/response/general-schemas');
const { GetAllFiltersResult, GetFiltersResult } = require('../schemas/response/filters-schemas');
const { FilterQuery, FilterAction } = require('../schemas/request/filters-schemas');
const { userId, filterId } = require('../schemas/request/general-schemas');
const { mongopagingFindWrapper } = require('../mongopaging-find-wrapper');
module.exports = (db, server, userHandler, settingsHandler) => {
server.get(
@ -29,7 +29,6 @@ module.exports = (db, server, userHandler, settingsHandler) => {
limit: Joi.number().default(20).min(1).max(250).description('How many records to return'),
next: nextPageCursorSchema,
previous: previousPageCursorSchema,
page: pageNrSchema,
sess: sessSchema,
ip: sessIPSchema
},
@ -90,7 +89,6 @@ module.exports = (db, server, userHandler, settingsHandler) => {
let forward = result.value.forward;
let limit = result.value.limit;
let page = result.value.page;
let pageNext = result.value.next;
let pagePrevious = result.value.previous;
@ -119,13 +117,14 @@ module.exports = (db, server, userHandler, settingsHandler) => {
if (pageNext) {
opts.next = pageNext;
} else if ((!page || page > 1) && pagePrevious) {
}
if (pagePrevious) {
opts.previous = pagePrevious;
}
let listing;
let listingWrapper;
try {
listing = await MongoPaging.find(db.database.collection('filters'), opts);
listingWrapper = await mongopagingFindWrapper(db.database.collection('filters'), opts);
} catch (err) {
res.status(500);
return res.json({
@ -134,13 +133,9 @@ module.exports = (db, server, userHandler, settingsHandler) => {
});
}
if (!listing.hasPrevious) {
page = 1;
}
let mailboxList = Array.from(
new Set(
(listing.results || [])
(listingWrapper.listing.results || [])
.map(filterData => {
if (filterData.action && filterData.action.mailbox) {
return filterData.action.mailbox.toString();
@ -174,10 +169,10 @@ module.exports = (db, server, userHandler, settingsHandler) => {
let response = {
success: true,
total,
page,
previousCursor: listing.hasPrevious ? listing.previous : false,
nextCursor: listing.hasNext ? listing.next : false,
results: (listing.results || []).map(filterData => {
page: listingWrapper.page,
previousCursor: listingWrapper.previousCursor,
nextCursor: listingWrapper.nextCursor,
results: (listingWrapper.listing.results || []).map(filterData => {
let descriptions = getFilterStrings(filterData, mailboxes);
let values = {

View file

@ -4,7 +4,6 @@ const config = require('wild-config');
const log = require('npmlog');
const libmime = require('libmime');
const Joi = require('joi');
const MongoPaging = require('mongo-cursor-pagination');
const addressparser = require('nodemailer/lib/addressparser');
const MailComposer = require('nodemailer/lib/mail-composer');
const { htmlToText } = require('html-to-text');
@ -17,7 +16,7 @@ const forward = require('../forward');
const Maildropper = require('../maildropper');
const util = require('util');
const roles = require('../roles');
const { nextPageCursorSchema, previousPageCursorSchema, pageNrSchema, sessSchema, sessIPSchema, booleanSchema, metaDataSchema } = require('../schemas');
const { nextPageCursorSchema, previousPageCursorSchema, sessSchema, sessIPSchema, booleanSchema, metaDataSchema } = require('../schemas');
const { preprocessAttachments } = require('../data-url');
const TaskHandler = require('../task-handler');
const { prepareSearchFilter, uidRangeStringToQuery } = require('../prepare-search-filter');
@ -38,6 +37,7 @@ const {
const { userId, mailboxId, messageId } = require('../schemas/request/general-schemas');
const { MsgEnvelope } = require('../schemas/response/messages-schemas');
const { successRes } = require('../schemas/response/general-schemas');
const { mongopagingFindWrapper } = require('../mongopaging-find-wrapper');
module.exports = (db, server, messageHandler, userHandler, storageHandler, settingsHandler) => {
let maildrop = new Maildropper({
@ -330,7 +330,6 @@ module.exports = (db, server, messageHandler, userHandler, storageHandler, setti
order: Joi.any().empty('').allow('asc', 'desc').default('desc').description('Ordering of the records by insert date'),
next: nextPageCursorSchema,
previous: previousPageCursorSchema,
page: pageNrSchema,
sess: sessSchema,
ip: sessIPSchema,
includeHeaders: Joi.alternatives()
@ -466,7 +465,6 @@ module.exports = (db, server, messageHandler, userHandler, storageHandler, setti
let mailbox = new ObjectId(result.value.mailbox);
let limit = result.value.limit;
let threadCounters = result.value.threadCounters;
let page = result.value.page;
let pageNext = result.value.next;
let pagePrevious = result.value.previous;
let sortAscending = result.value.order === 'asc';
@ -569,13 +567,14 @@ module.exports = (db, server, messageHandler, userHandler, storageHandler, setti
if (pageNext) {
opts.next = pageNext;
} else if ((!page || page > 1) && pagePrevious) {
}
if (pagePrevious) {
opts.previous = pagePrevious;
}
let listing;
let listingWrapper;
try {
listing = await MongoPaging.find(db.database.collection('messages'), opts);
listingWrapper = await mongopagingFindWrapper(db.database.collection('messages'), opts);
} catch (err) {
res.status(500);
return res.json({
@ -584,24 +583,20 @@ module.exports = (db, server, messageHandler, userHandler, storageHandler, setti
});
}
if (!listing.hasPrevious) {
page = 1;
}
if (threadCounters) {
listing.results = await addThreadCountersToMessageList(user, listing.results);
listingWrapper.listing.results = await addThreadCountersToMessageList(user, listingWrapper.listing.results);
}
await applyBimiToListing(listing.results);
await applyBimiToListing(listingWrapper.listing.results);
let response = {
success: true,
total,
page,
previousCursor: listing.hasPrevious ? listing.previous : false,
nextCursor: listing.hasNext ? listing.next : false,
page: listingWrapper.page,
previousCursor: listingWrapper.previousCursor,
nextCursor: listingWrapper.nextCursor,
specialUse: mailboxData.specialUse,
results: (listing.results || []).map(entry => formatMessageListing(entry, includeHeaders))
results: (listingWrapper.listing.results || []).map(entry => formatMessageListing(entry, includeHeaders))
};
return res.json(response);
@ -681,8 +676,7 @@ module.exports = (db, server, messageHandler, userHandler, storageHandler, setti
.example('List-ID, MIME-Version')
.description('Comma separated list of header keys to include in the response'),
next: nextPageCursorSchema,
previous: previousPageCursorSchema,
page: pageNrSchema
previous: previousPageCursorSchema
}
},
pathParams: { user: userId },
@ -792,7 +786,6 @@ module.exports = (db, server, messageHandler, userHandler, storageHandler, setti
let user = new ObjectId(result.value.user);
let threadCounters = result.value.threadCounters;
let limit = result.value.limit;
let page = result.value.page;
let pageNext = result.value.next;
let pagePrevious = result.value.previous;
let order = result.value.order;
@ -890,13 +883,14 @@ module.exports = (db, server, messageHandler, userHandler, storageHandler, setti
if (pageNext) {
opts.next = pageNext;
} else if ((!page || page > 1) && pagePrevious) {
}
if (pagePrevious) {
opts.previous = pagePrevious;
}
let listing;
let listingWrapper;
try {
listing = await MongoPaging.find(db.database.collection('messages'), opts);
listingWrapper = await mongopagingFindWrapper(db.database.collection('messages'), opts);
} catch (err) {
res.status(500);
return res.json({
@ -905,24 +899,20 @@ module.exports = (db, server, messageHandler, userHandler, storageHandler, setti
});
}
if (!listing.hasPrevious) {
page = 1;
}
if (threadCounters) {
listing.results = await addThreadCountersToMessageList(user, listing.results);
listingWrapper.listing.results = await addThreadCountersToMessageList(user, listingWrapper.listing.results);
}
await applyBimiToListing(listing.results);
await applyBimiToListing(listingWrapper.listing.results);
let response = {
success: true,
query,
total,
page,
previousCursor: listing.hasPrevious ? listing.previous : false,
nextCursor: listing.hasNext ? listing.next : false,
results: (listing.results || []).map(entry => formatMessageListing(entry, includeHeaders))
page: listingWrapper.page,
previousCursor: listingWrapper.previousCursor,
nextCursor: listingWrapper.nextCursor,
results: (listingWrapper.listing.results || []).map(entry => formatMessageListing(entry, includeHeaders))
};
return res.json(response);
@ -3251,7 +3241,6 @@ module.exports = (db, server, messageHandler, userHandler, storageHandler, setti
.empty('')
.example('List-ID, MIME-Version')
.description('Comma separated list of header keys to include in the response'),
page: pageNrSchema,
sess: sessSchema,
ip: sessIPSchema
},
@ -3360,7 +3349,6 @@ module.exports = (db, server, messageHandler, userHandler, storageHandler, setti
let user = new ObjectId(result.value.user);
let limit = result.value.limit;
let page = result.value.page;
let pageNext = result.value.next;
let pagePrevious = result.value.previous;
let sortAscending = result.value.order === 'asc';
@ -3421,13 +3409,14 @@ module.exports = (db, server, messageHandler, userHandler, storageHandler, setti
if (pageNext) {
opts.next = pageNext;
} else if ((!page || page > 1) && pagePrevious) {
}
if (pagePrevious) {
opts.previous = pagePrevious;
}
let listing;
let listingWrapper;
try {
listing = await MongoPaging.find(db.database.collection('archived'), opts);
listingWrapper = await mongopagingFindWrapper(db.database.collection('archived'), opts);
} catch (err) {
res.status(500);
return res.json({
@ -3436,17 +3425,13 @@ module.exports = (db, server, messageHandler, userHandler, storageHandler, setti
});
}
if (!listing.hasPrevious) {
page = 1;
}
let response = {
success: true,
total,
page,
previousCursor: listing.hasPrevious ? listing.previous : false,
nextCursor: listing.hasNext ? listing.next : false,
results: (listing.results || [])
page: listingWrapper.page,
previousCursor: listingWrapper.previousCursor,
nextCursor: listingWrapper.nextCursor,
results: (listingWrapper.listing.results || [])
.map(m => {
// prepare message for output
m.uid = m._id;

View file

@ -1,14 +1,14 @@
'use strict';
const Joi = require('joi');
const MongoPaging = require('mongo-cursor-pagination');
const ObjectId = require('mongodb').ObjectId;
const tools = require('../tools');
const roles = require('../roles');
const consts = require('../consts');
const { nextPageCursorSchema, previousPageCursorSchema, pageNrSchema, sessSchema, sessIPSchema } = require('../schemas');
const { nextPageCursorSchema, previousPageCursorSchema, sessSchema, sessIPSchema } = require('../schemas');
const { userId } = require('../schemas/request/general-schemas');
const { successRes, totalRes, pageRes, previousCursorRes, nextCursorRes } = require('../schemas/response/general-schemas');
const { mongopagingFindWrapper } = require('../mongopaging-find-wrapper');
module.exports = (db, server, storageHandler) => {
server.post(
@ -137,7 +137,6 @@ module.exports = (db, server, storageHandler) => {
limit: Joi.number().default(20).min(1).max(250).description('How many records to return'),
next: nextPageCursorSchema,
previous: previousPageCursorSchema,
page: pageNrSchema,
sess: sessSchema,
ip: sessIPSchema
},
@ -238,7 +237,6 @@ module.exports = (db, server, storageHandler) => {
let query = result.value.query;
let limit = result.value.limit;
let page = result.value.page;
let pageNext = result.value.next;
let pagePrevious = result.value.previous;
@ -263,13 +261,14 @@ module.exports = (db, server, storageHandler) => {
if (pageNext) {
opts.next = pageNext;
} else if ((!page || page > 1) && pagePrevious) {
}
if (pagePrevious) {
opts.previous = pagePrevious;
}
let listing;
let listingWrapper;
try {
listing = await MongoPaging.find(db.gridfs.collection('storage.files'), opts);
listingWrapper = await mongopagingFindWrapper(db.gridfs.collection('storage.files'), opts);
} catch (err) {
res.status(500);
return res.json({
@ -278,18 +277,14 @@ module.exports = (db, server, storageHandler) => {
});
}
if (!listing.hasPrevious) {
page = 1;
}
let response = {
success: true,
query,
total,
page,
previousCursor: listing.hasPrevious ? listing.previous : false,
nextCursor: listing.hasNext ? listing.next : false,
results: (listing.results || []).map(fileData => ({
page: listingWrapper.page,
previousCursor: listingWrapper.previousCursor,
nextCursor: listingWrapper.nextCursor,
results: (listingWrapper.listing.results || []).map(fileData => ({
id: fileData._id.toString(),
filename: fileData.filename || undefined,
contentType: fileData.contentType || undefined,

View file

@ -3,7 +3,6 @@
const log = require('npmlog');
const config = require('wild-config');
const Joi = require('joi');
const MongoPaging = require('mongo-cursor-pagination');
const ObjectId = require('mongodb').ObjectId;
const tools = require('../tools');
const errors = require('../errors');
@ -13,22 +12,14 @@ const consts = require('../consts');
const roles = require('../roles');
const imapTools = require('../../imap-core/lib/imap-tools');
const pwnedpasswords = require('pwnedpasswords');
const {
nextPageCursorSchema,
previousPageCursorSchema,
pageNrSchema,
sessSchema,
sessIPSchema,
booleanSchema,
metaDataSchema,
usernameSchema
} = require('../schemas');
const { nextPageCursorSchema, previousPageCursorSchema, sessSchema, sessIPSchema, booleanSchema, metaDataSchema, usernameSchema } = require('../schemas');
const TaskHandler = require('../task-handler');
const { publish, FORWARD_ADDED } = require('../events');
const { ExportStream, ImportStream } = require('../export');
const { successRes, totalRes, pageRes, previousCursorRes, nextCursorRes, quotaRes } = require('../schemas/response/general-schemas');
const { GetUsersResult } = require('../schemas/response/users-schemas');
const { userId } = require('../schemas/request/general-schemas');
const { mongopagingFindWrapper } = require('../mongopaging-find-wrapper');
const FEATURE_FLAGS = ['indexing'];
@ -58,7 +49,6 @@ module.exports = (db, server, userHandler, settingsHandler) => {
limit: Joi.number().default(20).min(1).max(250).description('How many records to return'),
next: nextPageCursorSchema,
previous: previousPageCursorSchema,
page: pageNrSchema,
sess: sessSchema,
ip: sessIPSchema
},
@ -120,7 +110,6 @@ module.exports = (db, server, userHandler, settingsHandler) => {
let forward = result.value.forward;
let limit = result.value.limit;
let page = result.value.page;
let pageNext = result.value.next;
let pagePrevious = result.value.previous;
@ -232,13 +221,14 @@ module.exports = (db, server, userHandler, settingsHandler) => {
if (pageNext) {
opts.next = pageNext;
} else if ((!page || page > 1) && pagePrevious) {
}
if (pagePrevious) {
opts.previous = pagePrevious;
}
let listing;
let listingWrapper;
try {
listing = await MongoPaging.find(db.users.collection('users'), opts);
listingWrapper = await mongopagingFindWrapper(db.users.collection('users'), opts);
} catch (err) {
res.status(500);
return res.json({
@ -247,20 +237,16 @@ module.exports = (db, server, userHandler, settingsHandler) => {
});
}
if (!listing.hasPrevious) {
page = 1;
}
let settings = await settingsHandler.getMulti(['const:max:storage']);
let response = {
success: true,
query,
total,
page,
previousCursor: listing.hasPrevious ? listing.previous : false,
nextCursor: listing.hasNext ? listing.next : false,
results: (listing.results || []).map(userData => {
page: listingWrapper.page,
previousCursor: listingWrapper.previousCursor,
nextCursor: listingWrapper.nextCursor,
results: (listingWrapper.listing.results || []).map(userData => {
let values = {
id: userData._id.toString(),
username: userData.username,

View file

@ -1,12 +1,12 @@
'use strict';
const Joi = require('joi');
const MongoPaging = require('mongo-cursor-pagination');
const ObjectId = require('mongodb').ObjectId;
const tools = require('../tools');
const roles = require('../roles');
const { nextPageCursorSchema, previousPageCursorSchema, pageNrSchema, sessSchema, sessIPSchema } = require('../schemas');
const { nextPageCursorSchema, previousPageCursorSchema, sessSchema, sessIPSchema } = require('../schemas');
const { successRes, totalRes, pageRes, previousCursorRes, nextCursorRes } = require('../schemas/response/general-schemas');
const { mongopagingFindWrapper } = require('../mongopaging-find-wrapper');
module.exports = (db, server) => {
server.get(
@ -27,7 +27,6 @@ module.exports = (db, server) => {
limit: Joi.number().default(20).min(1).max(250).description('How many records to return'),
next: nextPageCursorSchema,
previous: previousPageCursorSchema,
page: pageNrSchema,
sess: sessSchema,
ip: sessIPSchema
},
@ -110,7 +109,6 @@ module.exports = (db, server) => {
}
let limit = result.value.limit;
let page = result.value.page;
let pageNext = result.value.next;
let pagePrevious = result.value.previous;
@ -137,13 +135,14 @@ module.exports = (db, server) => {
if (pageNext) {
opts.next = pageNext;
} else if ((!page || page > 1) && pagePrevious) {
}
if (pagePrevious) {
opts.previous = pagePrevious;
}
let listing;
let listingWrapper;
try {
listing = await MongoPaging.find(db.users.collection('webhooks'), opts);
listingWrapper = await mongopagingFindWrapper(db.users.collection('webhooks'), opts);
} catch (err) {
res.status(500);
return res.json({
@ -152,19 +151,15 @@ module.exports = (db, server) => {
});
}
if (!listing.hasPrevious) {
page = 1;
}
let response = {
success: true,
type: result.value.type,
user,
total,
page,
previousCursor: listing.hasPrevious ? listing.previous : false,
nextCursor: listing.hasNext ? listing.next : false,
results: (listing.results || []).map(webhookData => {
page: listingWrapper.page,
previousCursor: listingWrapper.previousCursor,
nextCursor: listingWrapper.nextCursor,
results: (listingWrapper.listing.results || []).map(webhookData => {
let values = {
id: webhookData._id.toString(),
type: webhookData.type,

View file

@ -0,0 +1,111 @@
'use strict';
const MongoPaging = require('mongo-cursor-pagination');
const { EJSON } = require('bson');
const mongopagingFindWrapper = async (collection, opts) => {
let currentPage = 1;
const pageNextOriginalCursor = getCursorFromCursorWrapper(opts.next);
const pagePrevOriginalCursor = getCursorFromCursorWrapper(opts.previous);
if (pageNextOriginalCursor) {
// Have next cursor
const pageFromNextCursor = getPageFromMongopagingCursor(opts.next);
opts.next = pageNextOriginalCursor; // For mongopaging only preserve the original inner cursor
currentPage = pageFromNextCursor;
} else if (pagePrevOriginalCursor) {
// Have prev cursor
delete opts.next; // Previous cursor overwrites next
const pageFromPreviousCursor = getPageFromMongopagingCursor(opts.previous);
opts.previous = pagePrevOriginalCursor; // For mongopaging only preserve the original inner cursor
currentPage = pageFromPreviousCursor;
}
const listing = await MongoPaging.find(collection, opts);
if (!listing.hasPrevious) {
currentPage = 1;
} else if (currentPage < 1) {
// Against crafted cursors with negative pages
currentPage = 1;
}
let nextCursor = listing.hasNext ? listing.next : false;
if (nextCursor) {
nextCursor = setPageToMongopagingCursor(nextCursor, currentPage + 1);
}
let previousCursor = listing.hasPrevious ? listing.previous : false;
if (previousCursor) {
previousCursor = setPageToMongopagingCursor(previousCursor, currentPage - 1 < 0 ? 0 : currentPage - 1);
}
return {
listing,
nextCursor,
previousCursor,
page: currentPage
};
};
function getPageFromMongopagingCursor(cursorString) {
if (!cursorString) {
return 1;
}
try {
const cursorObjStr = EJSON.deserialize(Buffer.from(cursorString, 'base64url').toString());
const cursorWrapperArr = EJSON.parse(cursorObjStr);
if (cursorWrapperArr.length >= 2) {
return cursorWrapperArr[1];
}
return 1; // Fallback
} catch {
return 1;
}
}
function setPageToMongopagingCursor(cursorString, page) {
if (!cursorString) {
return false;
}
try {
const cursorWrapperArr = [];
cursorWrapperArr.push(Buffer.from(cursorString, 'base64url').toString()); // Preserve original string. Decode base64url to not double base64url encode
cursorWrapperArr.push(page);
const newCursorString = Buffer.from(EJSON.stringify(EJSON.serialize(cursorWrapperArr))).toString('base64url');
return newCursorString;
} catch {
return false;
}
}
function getCursorFromCursorWrapper(cursorWrapperString) {
if (!cursorWrapperString) {
return false;
}
try {
const cursorObjStr = EJSON.deserialize(Buffer.from(cursorWrapperString, 'base64url').toString());
const cursorWrapperArr = EJSON.parse(cursorObjStr);
return Buffer.from(cursorWrapperArr[0]).toString('base64url');
} catch {
return false;
}
}
module.exports = {
mongopagingFindWrapper,
getPageFromMongopagingCursor,
setPageToMongopagingCursor,
getCursorFromCursorWrapper
};

View file

@ -41,7 +41,7 @@ const mongoCursorValidator = () => (value, helpers) => {
return helpers.error('any.invalid');
}
try {
EJSON.parse(Buffer.from(value, 'base64'));
EJSON.parse(Buffer.from(value, 'base64url'));
} catch (E) {
return helpers.error('any.invalid');
}