var hashToUser = function(hash) { // Ensure that the timezone we're setting is something the app can deal with. var timezone = hash.timezone; try { var date = new TZ.Date(null, timezone); if (!date.getTimezone()) { throw new Error(); } } catch (err) { // We can't deal with this timezone. // default to UTC timezone = 'Etc/UTC'; } var user = new User(hash.tenantAlias, hash.principalId, hash.displayName, { 'visibility': hash.visibility, 'email': hash.email, 'locale': hash.locale, 'timezone': timezone, 'publicAlias': hash.publicAlias, 'isGlobalAdmin': sanitize(hash['admin:global']).toBooleanStrict(), 'isTenantAdmin': sanitize(hash['admin:tenant']).toBooleanStrict(), 'smallPictureUri': hash.smallPictureUri, 'mediumPictureUri': hash.mediumPictureUri, 'largePictureUri': hash.largePictureUri, 'notificationsUnread': OaeUtil.getNumberParam(hash.notificationsUnread), 'notificationsLastRead': OaeUtil.getNumberParam(hash.notificationsLastRead) }); var extra = getExtraData(user, hash); if (extra) { user.extra = extra; } return user; };
var _createUpdatedDiscussionFromStorageHash = function(discussion, hash) { return new Discussion( discussion.tenant, discussion.id, discussion.createdBy, hash.displayName || discussion.displayName, hash.description || discussion.description, hash.visibility || discussion.visibility, OaeUtil.getNumberParam(discussion.created), OaeUtil.getNumberParam(hash.lastModified || discussion.lastModified) ); };
var _storageHashToDiscussion = function(discussionId, hash) { return new Discussion( TenantsAPI.getTenant(hash.tenantAlias), discussionId, hash.createdBy, hash.displayName, hash.description, hash.visibility, OaeUtil.getNumberParam(hash.created), OaeUtil.getNumberParam(hash.lastModified) ); };
const searchTenants = function(q, opts) { q = _.isString(q) ? q.trim() : null; opts = opts || {}; opts.start = OaeUtil.getNumberParam(opts.start, 0); // Determine if we should included disabled/deleted tenants const includeDisabled = _.isBoolean(opts.disabled) ? opts.disabled : false; // Create a sorted result of tenants based on the user's query. If there was no query, we will // pull the pre-sorted list of tenants from the global cache let results = null; if (q) { results = _.chain(tenantSearchIndex.search(q)) .sortBy('ref') .sortBy('score') .pluck('ref') .map(getTenant) .value(); } else { results = tenantsSorted; } results = _.filter(results, result => { if (result.isGlobalAdminServer) { return false; } if (!includeDisabled) { return result.active && !result.deleted; } return true; }); // Keep track of how many results we had in total const total = _.size(results); // Determine the end of our page slice opts.limit = OaeUtil.getNumberParam(opts.limit, total); const end = opts.start + opts.limit; // Cut down to just the requested page, and clone the tenants to avoid tenants being updated // in the cache results = results.slice(opts.start, end); results = _.map(results, _copyTenant); return { total, results }; };
var _createUpdatedMeetingFromStorageHash = function(meeting, hash) { return new Meeting( meeting.tenant, meeting.id, meeting.createdBy, hash.displayName || meeting.displayName, hash.description || meeting.description, hash.record || meeting.record, hash.allModerators || meeting.allModerators, hash.waitModerator || meeting.waitModerator, hash.visibility || meeting.visibility, OaeUtil.getNumberParam(meeting.created), OaeUtil.getNumberParam(hash.lastModified || meeting.lastModified) ); };
var _storageHashToMeeting = function(meetingId, hash) { return new Meeting( TenantsAPI.getTenant(hash.tenantAlias), meetingId, hash.createdBy, hash.displayName, hash.description, hash.record, hash.allModerators, hash.waitModerator, hash.visibility, OaeUtil.getNumberParam(hash.created), OaeUtil.getNumberParam(hash.lastModified) ); };
var searchCallback = function(ctx, opts, callback) { // Sanitize the custom search options opts = opts || {}; opts.principalId = opts.pathParams[0]; opts.limit = OaeUtil.getNumberParam(opts.limit, 12, 1, 25); var validator = new Validator(); validator.check(opts.principalId, {'code': 400, 'msg': 'Must specificy an id of a user or group to search'}).notEmpty(); if (validator.hasErrors()) { return callback(validator.getFirstError()); } var authzPrincipal = AuthzUtil.getPrincipalFromId(opts.principalId); if (ctx.user() && (ctx.user().isAdmin(authzPrincipal.tenantAlias) || ctx.user().id === opts.principalId)) { // Perform the search with full access when the current user is an administrator of (or *is*) the target principal _search(ctx, resourceType, true, opts, callback); } else if (ctx.user() && PrincipalsUtil.isGroup(opts.principalId)) { // If we're searching a group library, assume full access to all resources in the group if the user is a member of the group AuthzAPI.hasAnyRole(ctx.user().id, opts.principalId, function(err, hasAnyRole) { if (err) { return callback(err); } _search(ctx, resourceType, hasAnyRole, opts, callback); }); } else { // Either we're anonymous or we're searching some other user's library _search(ctx, resourceType, false, opts, callback); } };
var getActivityStream = module.exports.getActivityStream = function(ctx, resourceId, activtyStreamType, start, limit, transformerType, callback) { var activityStream = getRegisteredActivityStreamType(activtyStreamType); transformerType = transformerType || ActivityConstants.transformerTypes.ACTIVITYSTREAMS; var validator = new Validator(); validator.check(null, {'code': 401, 'msg': 'Must be logged in to see an activity stream'}).isLoggedInUser(ctx); validator.check(activtyStreamType, {'code': 400, 'msg': 'Must specify an activity stream'}).notEmpty(); validator.check(resourceId, {'code': 400, 'msg': 'You can only view activity streams for a principal'}).isPrincipalId(); validator.check(null, {'code': 400, 'msg': 'Unknown activity stream id'}).isObject(activityStream); validator.check(transformerType, {'code': 400, 'msg': 'Unknown activity transformer type'}).isIn(_.values(ActivityConstants.transformerTypes)); if (validator.hasErrors()) { return callback(validator.getFirstError()); } limit = OaeUtil.getNumberParam(limit, 25, 1); // Ensure the current user has access to this stream var authorizationHandler = activityStream.authorizationHandler || function(ctx, activtyStreamType, token, callback) { return callback(); }; authorizationHandler(ctx, resourceId, null, function(err) { if (err) { return callback(err); } return _getActivityStream(ctx, resourceId + '#' + activtyStreamType, start, limit, transformerType, callback); }); };
var getQueuedActivities = module.exports.getQueuedActivities = function(bucketNumber, limit, callback) { limit = OaeUtil.getNumberParam(limit, ActivitySystemConfig.getConfig().collectionBatchSize); // Get the first `limit` routed activities from the bucket. Since they are stored in a sorted list in Redis, we use the // "zrange" command. The "z" prefix to the command indicates that it is a sorted-list operation. redisClient.zrange(_createBucketCacheKey(bucketNumber), 0, limit, function(err, routedActivities) { if (err) { return callback(err); } // The Redis result is each value on a new line, in order of "rank" in the sorted-list. Iterate over those and parse the values in order. var queuedActivities = {}; _.each(routedActivities, function(routedActivity) { try { // Routed activities are stored as stringified JSON, so we parse them back to objects routedActivity = JSON.parse(routedActivity); } catch (err) { log().warn({'err': err, 'routedActivity': routedActivity}, 'Error trying to parse stored routed activity.'); return; } queuedActivities[_createRoutedActivityKey(routedActivity)] = routedActivity; }); log().trace({'queuedActivities': queuedActivities}, 'Fetched queued activities.'); return callback(null, queuedActivities); }); };
var createTestServer = module.exports.createTestServer = function(callback, _attempts) { _attempts = OaeUtil.getNumberParam(_attempts, 0); if (_attempts === 10) { assert.fail('Could not start a test web server in 10 attempts'); } var port = 2500 + Math.floor(Math.random() * 1000); var app = express(); app.use(bodyParser.urlencoded({'extended': true})); app.use(bodyParser.json()); app.use(multipart()); // Try and listen on the specified port var server = app.listen(port + _attempts); // When the server successfully begins listening, invoke the callback server.once('listening', function() { server.removeAllListeners('error'); return callback(app, server, port + _attempts); }); // If there is an error connecting, try another port server.once('error', function(err) { server.removeAllListeners('listening'); return createTestServer(callback, _attempts + 1); }); };
module.exports = function(ctx, opts, callback) { // Sanitize custom search options opts = opts || {}; opts.limit = OaeUtil.getNumberParam(opts.limit, 10, 1, 25); var validator = new Validator(); validator.check(null, {'code': 401, 'msg': 'Only authenticated users can use email search'}).isLoggedInUser(ctx); validator.check(opts.q, {'code': 400, 'msg': 'An invalid email address has been specified'}).isEmail(); if (validator.hasErrors()) { return callback(validator.getFirstError()); } // Ensure the email address being searched is lower case so it is case insensitive var email = opts.q.toLowerCase(); var filterResources = SearchUtil.filterResources(['user']); var filterInteractingTenants = SearchUtil.filterInteractingTenants(ctx.user().tenant.alias); // When searching for users by email, we can ignore profile visibility in lieu of an email // exact match. The user profile is still "scrubbed" of private information on its way out, // however we enable the ability for a user to share with that profile if they know the email // address var query = SearchUtil.createEmailQuery(email); var queryOpts = _.extend({}, opts, {'minScore': 0}); var filter = SearchUtil.filterAnd(filterResources, filterInteractingTenants); return callback(null, SearchUtil.createQuery(query, filter, queryOpts)); };
var getMembershipsLibrary = module.exports.getMembershipsLibrary = function(ctx, principalId, start, limit, callback) { limit = OaeUtil.getNumberParam(limit, 10, 1); var validator = new Validator(); validator.check(principalId, {'code': 400, 'msg': 'Must specify a valid principalId'}).isPrincipalId(); if (validator.hasErrors()) { return callback(validator.getFirstError()); } PrincipalsDAO.getPrincipal(principalId, function(err, principal) { if (err) { return callback(err); } else if (principal.deleted) { return callback({'code': 404, 'msg': util.format('Couldn\'t find principal: %s', principalId)}); } LibraryAPI.Authz.resolveTargetLibraryAccess(ctx, principal.id, principal, function(err, hasAccess, visibility) { if (err) { return callback(err); } else if (!hasAccess) { return callback({'code': 401, 'msg': 'You do not have access to this memberships library'}); } return _getMembershipsLibrary(ctx, principalId, visibility, start, limit, callback); }); }); };
RestAPI.Activity.markNotificationsRead(restContext, function(err, _result) { assert.ok(!err); // Assert we're getting back a number result = _result; var lastReadTime = _result.lastReadTime; assert.strictEqual(lastReadTime, OaeUtil.getNumberParam(lastReadTime)); });
var _storageHashToDiscussion = function(discussionId, hash) { // Use tenantAlias as a slug column to determine if this discussion actually existed if (!hash.tenantAlias) { return null; } return new Discussion( TenantsAPI.getTenant(hash.tenantAlias), discussionId, hash.createdBy, hash.displayName, hash.description, hash.visibility, OaeUtil.getNumberParam(hash.created), OaeUtil.getNumberParam(hash.lastModified) ); };
OAE.tenantRouter.on('get', '/api/discussion/library/:principalId', function(req, res) { var limit = Util.getNumberParam(req.query.limit, 12, 1, 25); DiscussionsAPI.Discussions.getDiscussionsLibrary(req.ctx, req.params.principalId, req.query.start, limit, function(err, discussions, nextToken) { if (err) { return res.send(err.code, err.msg); } res.send(200, {'results': discussions, 'nextToken': nextToken}); }); });
OAE.tenantRouter.on('get', '/api/discussion/:discussionId/messages', function(req, res) { var limit = Util.getNumberParam(req.query.limit, 10, 1, 25); DiscussionsAPI.Discussions.getMessages(req.ctx, req.params.discussionId, req.query.start, limit, function(err, messages, nextToken) { if (err) { return res.send(err.code, err.msg); } res.send(200, {'results': messages, 'nextToken': nextToken}); }); });
var _hashToUser = function(hash) { var user = new User(hash.tenantAlias, hash.principalId, hash.displayName, hash.email, { 'visibility': hash.visibility, 'deleted': hash.deleted, 'locale': hash.locale, 'publicAlias': hash.publicAlias, 'isGlobalAdmin': sanitize(hash['admin:global']).toBooleanStrict(), 'isTenantAdmin': sanitize(hash['admin:tenant']).toBooleanStrict(), 'smallPictureUri': hash.smallPictureUri, 'mediumPictureUri': hash.mediumPictureUri, 'largePictureUri': hash.largePictureUri, 'notificationsUnread': OaeUtil.getNumberParam(hash.notificationsUnread), 'notificationsLastRead': OaeUtil.getNumberParam(hash.notificationsLastRead), 'emailPreference': hash.emailPreference || PrincipalsConfig.getValue(hash.tenantAlias, 'user', 'emailPreference'), 'acceptedTC': OaeUtil.getNumberParam(hash.acceptedTC, 0), 'lastModified': OaeUtil.getNumberParam(hash.lastModified) }); return user; };
OAE.tenantRouter.on('get', '/api/following/:userId/following', function(req, res) { var limit = OaeUtil.getNumberParam(req.query.limit, 10, 1, 25); FollowingAPI.getFollowing(req.ctx, req.params.userId, req.query.start, limit, function(err, following, nextToken) { if (err) { return res.status(err.code).send(err.msg); } return res.status(200).send({'results': following, 'nextToken': nextToken}); }); });
module.exports = function(ctx, opts, callback) { // Sanitize custom search options opts = opts || {}; opts.includeIndirect = (opts.includeIndirect !== 'false'); opts.limit = OaeUtil.getNumberParam(opts.limit, 10, 1, 25); opts.q = SearchUtil.getQueryParam(opts.q); opts.resourceTypes = _getResourceTypesParam(opts.resourceTypes); opts.searchAllResourceTypes = (_.isEmpty(opts.resourceTypes)); return _search(ctx, opts, callback); };
OAE.tenantServer.get('/api/notifications', function(req, res) { req.telemetryUrl = '/api/notifications'; var limit = OaeUtil.getNumberParam(req.query.limit, 10, 1, 25); ActivityAPI.getNotificationStream(req.ctx, req.query.start, limit, function(err, notificationStream) { if (err) { return res.send(err.code, err.msg); } res.send(200, notificationStream); }); });
var createQuery = module.exports.createQuery = function(query, filter, opts) { opts = opts || {}; var validator = new Validator(); validator.check(null, new Error('createQuery expects a query object.')).isObject(query); if (validator.hasErrors()) { log().error({'err': validator.getFirstError()}, 'Invalid input provided to SearchUtil.createQuery'); throw validator.getFirstError(); } var data = null; if (filter) { // If we have filters, we need to create a 'filtered' query data = { 'query': { 'filtered': { 'query': query, 'filter': filter } } }; } else { // If it's just a query, we wrap it in a standard query. data = { 'query': query }; } // Strip the opts down to the relevant ElasticSearch parameters opts = { 'from': OaeUtil.getNumberParam(opts.start), 'size': OaeUtil.getNumberParam(opts.limit), 'sort': [ {'_score': {'order': 'desc'}}, {'sort': getSortParam(opts.sort)} ], 'min_score': (_.isNumber(opts.minScore)) ? opts.minScore : SearchConstants.query.MINIMUM_SCORE }; return _.extend(data, opts); };
var _handleGetActivities = function(activityStreamId, req, res) { req.telemetryUrl = '/api/activity/activityStreamId'; var limit = OaeUtil.getNumberParam(req.query.limit, 10, 1, 25); var start = req.query.start; ActivityAPI.getActivityStream(req.ctx, activityStreamId, start, limit, function(err, activityStream) { if (err) { return res.send(err.code, err.msg); } res.send(200, activityStream); }); };
const init = function(_config, callback) { _config = _config || {}; screenShottingOptions.timeout = OaeUtil.getNumberParam(_config.screenShotting.timeout, screenShottingOptions.timeout); const chromiumExecutable = _config.screenShotting.binary; if (chromiumExecutable) { screenShottingOptions.executablePath = chromiumExecutable; } return callback(); };
var getActivities = module.exports.getActivities = function(activityStreamId, start, limit, callback) { start = start || ''; limit = OaeUtil.getNumberParam(limit, 25, 1); // Selecting with consistency ONE as having great consistency is not critical for activities Cassandra.runPagedQuery('ActivityStreams', 'activityStreamId', activityStreamId, 'activityId', start, limit, {'reversed': true}, function(err, rows, nextToken) { if (err) { return callback(err); } var activities = _rowsToActivities(rows); return callback(null, activities, nextToken); }); };
var generateRandomText = module.exports.generateRandomText = function(nrOfWords) { nrOfWords = OaeUtil.getNumberParam(nrOfWords, 1, 1); var alphabet = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; var text = []; for (var i = 0; i < nrOfWords; i++) { var wordLength = 12; var word = ''; for (var l = 0; l < wordLength; l++) { var letter = Math.floor(Math.random() * alphabet.length); word += alphabet[letter]; } text.push(word); } return text.join(' '); };
module.exports = function(ctx, opts, callback) { // Sanitize custom search options opts = opts || {}; opts.resourceTypes = opts.resourceTypes || []; opts.includeExternal = (opts.includeExternal === 'true'); opts.includeIndirect = (opts.includeIndirect !== 'false'); opts.limit = OaeUtil.getNumberParam(opts.limit, 10, 1, 25); // Sanitize the resourceTypes array if (!_.isArray(opts.resourceTypes)) { // Convert to an array if it's a single value opts.resourceTypes = [opts.resourceTypes]; } // Remove any falsy values from the array opts.resourceTypes = _.compact(opts.resourceTypes); // If there were no valid values for resource type, we search everything opts.searchAllResourceTypes = _.isEmpty(opts.resourceTypes); // opts.q is required to determine _needsFilterByAccess opts.q = SearchUtil.getQueryParam(opts.q); var needsFilterByAccess = _needsFilterByAccess(ctx, opts); if (needsFilterByAccess && opts.includeIndirect) { // We'll need to know the group membership of this user to scope by resources they have access to directly or indirectly AuthzAPI.getPrincipalMemberships(ctx.user().id, null, 10000, function(err, groups) { if (err) { return callback(err); } // Bind the access array to the search options var access = groups || []; access.push(ctx.user().id); opts.access = access; _search(ctx, opts, callback); }); } else if (needsFilterByAccess && !opts.includeIndirect) { // If we're not including indirect resources, only filter by direct user access opts.access = [ctx.user().id]; _search(ctx, opts, callback); } else { _search(ctx, opts, callback); } };
var getActivities = module.exports.getActivities = function(activityStreamId, start, limit, callback) { limit = OaeUtil.getNumberParam(limit, 25); // Selecting with consistency ONE as having great consistency is not critical for activities Cassandra.runPagedColumnQuery('ActivityStreams', 'activityStreamId', activityStreamId, start, limit, {'reversed': true, 'consistency': 'ONE'}, function(err, columns, nextToken) { if (err) { return callback(err); } var activities = _columnsToActivities(columns); /* * The following block of code is intended for migrating from 3.0.0 to 4.0.0 (Push) * The `activityStreamId` for "activity" activity streams is/was of the form: * - 4.0.0: `u:cam:abc123#activity` or `g:cam:abc123#activity` * - 3.0.0: `u:cam:abc123` or `g:cam:abc123` * * If we detect that a user activity stream is being requested under the following circumstances, * we will try the old value as the `activityStreamId`. */ // We only need to check with another activityStreamId if the "activity" stream is being requested. Notifications are unchanged var parsedActivityStreamId = ActivityUtil.parseActivityStreamId(activityStreamId); var isActivityStream = (parsedActivityStreamId.streamType === 'activity'); if (!isActivityStream) { return callback(null, activities, nextToken); } // If we found the requested amount of activities we can return early if (activities.length === limit) { return callback(null, activities, nextToken); } // Otherwise we'll need to check the old value var oldActivityStreamLimit = limit - activities.length; Cassandra.runPagedColumnQuery('ActivityStreams', 'activityStreamId', parsedActivityStreamId.resourceId, start, oldActivityStreamLimit, {'reversed': true, 'consistency': 'ONE'}, function(err, columns, nextToken) { if (err) { return callback(err); } activities = activities.concat(_columnsToActivities(columns)); return callback(null, activities, nextToken); }); }); };
var generateTestUsers = module.exports.generateTestUsers = function(restCtx, total, callback, _createdUsers) { total = OaeUtil.getNumberParam(total, 1); _createdUsers = _createdUsers || []; if (total === 0) { var callbackArgs = []; callbackArgs.push(null); callbackArgs.push(_.indexBy(_createdUsers, function(user) { return user.user.id; })); callbackArgs = _.union(callbackArgs, _createdUsers); return callback.apply(callback, callbackArgs); } // Ensure that the provided rest context has been authenticated before trying to use it to // create users _ensureAuthenticated(restCtx, function(err) { if (err) { return callback(err); } var username = generateTestUserId('random-user'); var displayName = generateTestGroupId('random-user'); var email = generateTestEmailAddress(username); RestAPI.User.createUser(restCtx, username, 'password', displayName, {'email': email}, function(err, user) { if (err) { return callback(err); } _createdUsers.push({ 'user': user, 'restContext': new RestContext(restCtx.host, { 'hostHeader': restCtx.hostHeader, 'username': username, 'userPassword': '******', 'strictSSL': restCtx.strictSSL }) }); // Recursively continue creating users return generateTestUsers(restCtx, --total, callback, _createdUsers); }); }); };
RestAPI.Activity.markNotificationsRead(simong.restContext, function(err, result) { var lastReadTime = result.lastReadTime; assert.strictEqual(lastReadTime, OaeUtil.getNumberParam(lastReadTime)); // Verify the notificationsLastRead status RestAPI.User.getMe(simong.restContext, function(err, me) { assert.ok(!err); // We now have no unread notifications, and a lastRead status assert.strictEqual(me.notificationsUnread, 0); assert.strictEqual(me.notificationsLastRead, lastReadTime); // Create 2 content items again with simong as a member RestAPI.Content.createLink(mrvisser.restContext, 'Google', 'Google', 'private', 'http://www.google.ca', [], [simong.user.id], function(err, content) { assert.ok(!err); RestAPI.Content.createLink(mrvisser.restContext, 'Google', 'Google', 'private', 'http://www.google.ca', [], [simong.user.id], function(err, content) { assert.ok(!err); // Ensure the notification gets delivered and aggregated ActivityTestsUtil.collectAndGetNotificationStream(simong.restContext, null, function(err, notificationStream) { assert.ok(!err); assert.equal(notificationStream.items.length, 1); assert.equal(notificationStream.items[0].actor['oae:id'], mrvisser.user.id); assert.equal(notificationStream.items[0].object['oae:collection'].length, 3); // Verify the notificationsUnread is incremented and notificationsLastRead status RestAPI.User.getMe(simong.restContext, function(err, me) { assert.ok(!err); // We now have unread notifications, and a lastRead status assert.strictEqual(me.notificationsUnread, 2); assert.equal(me.notificationsLastRead, lastReadTime); callback(); }); }); }); }); }); });
var getMembersLibrary = module.exports.getMembersLibrary = function(ctx, groupId, start, limit, callback) { limit = OaeUtil.getNumberParam(limit, 10, 1); var validator = new Validator(); validator.check(groupId, {'code': 400,'msg': 'An invalid group id was specified'}).isGroupId(); if (validator.hasErrors()) { return callback(validator.getFirstError()); } // Ensure that this group exists getGroup(ctx, groupId, function(err, group) { if (err) { return callback(err); } else if (group.deleted) { return callback({'code': 404, 'msg': util.format('Couldn\'t find principal: %s', groupId)}); } // Get the members library to which the current user has access return _getMembersLibrary(ctx, group, null, start, limit, callback); }); };