<commit_before>const service = require('feathers-mongoose');
const { Forbidden, NotFound, BadRequest } = require('@feathersjs/errors');
const logger = require('winston');
const { ObjectId } = require('mongoose').Types;
const {
	newsModel, targetModels, newsHistoryModel, newsPermissions,
} = require('./model');
const hooks = require('./hooks');
const newsModelHooks = require('./hooks/newsModel.hooks');
const { flatten } = require('../../utils/array');

class AbstractService {
	setup(app) {
		this.app = app;
	}

	/**
	* Returns school query if the user is allowed to see.
	* @param userId The user's Id
	* @param schoolId The schoolId
	* @returns Query
	*/
	createSchoolQuery(userId, schoolId, permission) {
		return this.hasSchoolPermission(userId, schoolId, permission)
			.then((hasPermission) => {
				if (!hasPermission) {
					return null;
				}
				return [{
					schoolId,
					target: { $exists: false },
				}];
			});
	}

	/**
		 * Returns scoped news the user is allowed to see
		 *
		 * @param {BsonId|String} userId the user's Id
		 * @param {BsonId|String} target (optional) Id of the news target (course, team, etc.)
		 * @returns Array<Query>
		 */
	async createScopedQuery(userId, permission, target = null, targetModel = null) {
		// check params
		if (target ? !targetModel : targetModel) {
			throw new BadRequest('target and targetModel, both must be given or not');
		}
		// only one target requested
		if (target && targetModel) {
			return this.hasPermission(userId, permission, { target, targetModel })
				.then(() => (
					[{
						targetModel,
						target,
					}]));
		}
		// return data of all user scopes
		const ops = targetModels.map(async (scope) => {
			// For each possible target model, find all targets the user has NEWS_VIEW permissions in.
			const scopeListService = this.app.service(`/users/:scopeId/${scope}`);
			if (scopeListService === undefined) {
				return null;
			}
			const scopeItems = await scopeListService.find({
				route: { scopeId: userId.toString() },
				query: {
					permissions: [permission],
				},
			});
			return scopeItems.map(item => ({
				targetModel: scope,
				target: item._id,
			}));
		});
		const results = await Promise.all(ops);
		return flatten(results.filter(r => r !== null));
	}

	/* Permissions */

	async getPermissions(userId, { target, targetModel, schoolId } = {}) {
		// target and school might be populated or not
		const isObjectId = o => o instanceof ObjectId || typeof o === 'string';
		// scope case: user role in scope must have given permission
		if (target && targetModel) {
			const targetId = isObjectId(target) ? target.toString() : target._id.toString();
			const scope = this.app.service(`${targetModel}/:scopeId/userPermissions/`);
			const params = { route: { scopeId: targetId } };
			const scopePermissions = await scope.get(userId, params);
			return scopePermissions;
		}

		// default school case: dataItem and users schoolId must match and user permission must exist
		return this.getSchoolPermissions(userId, isObjectId(schoolId) ? schoolId : schoolId._id);
	}

	/**
	 * Checks scoped permission for a user and given news.
	 * @param {BsonId|String} userId
	 * @param {String} permission
	 * @param {BsonId|String} targetId (optional) news target (scope) id. Only valid with targetModel.
	 * @param {String} targetModel (optional) news target (scope) model. Only valid with target.
	 * @returns {Promise<Boolean>} Promise that resolves to true/false
	 * @example
	 * await hasPermission(user._id, 'NEWS_VIEW') => true
	 * await hasPermission(user._id, 'NEWS_CREATE', team._id, 'teams') => false
	 * @memberof NewsService
	 */
	async hasPermission(userId, permission, dataItem) {
		if (!dataItem) {
			// maybe, use hasSchoolPermission instead...
			return false;
		}
		const permissions = await this.getPermissions(userId, dataItem);
		return permissions.includes(permission);
	}

	/**
	 * Tests if a user with given userId has a (global) permission within a school.
	 * @param {ObjectId} userId
	 * @param {ObjectId} schoolId
	 * @returns {Array<String>}
	 */
	async getSchoolPermissions(userId, schoolId) {
		// test user exists
		const user = await this.app.service('users').get(userId);
		if (user == null) return [];

		// test user is school member
		const sameSchool = schoolId.toString() === user.schoolId.toString();
		if (!sameSchool) return [];

		return user.permissions;
	}

	async hasSchoolPermission(userId, schoolId, permision) {
		const permissions = await this.getSchoolPermissions(userId, schoolId);
		return permissions.includes(permision);
	}

	/**
	 * Throw an error if the user is not allowed to perform the given operation
	 * @param {News} news news item (required: {schoolId}, optional: {target, targetModel})
	 * @param {Object} { userId, schoolId }
	 * @param {String} permission permision to check
	 * @returns undefined
	 * @example
	 * await authorize(news, params.account, 'NEWS_VIEW') => undefined
	 * await authorize(news, params.account, 'NEWS_CREATE') => (throws Forbidden)
	 * @memberof NewsService
	 */
	async authorize(news = {}, userId, permission) {
		const authorized = await this.hasPermission(userId, permission, news);
		if (!authorized) {
			throw new Forbidden('Not authorized.');
		}
	}

	checkExistence(news, query) {
		if (!news) {
			logger.error(`Cannot find news item with query "${query}" => "${news}"`);
			throw new NotFound('News item does not exist.');
		}
	}
}

class NewsService extends AbstractService {
	static populateParams() {
		return {
			query: {
				$populate: [
					{ path: 'schoolId', select: ['_id', 'name'] },
					{ path: 'creatorId', select: ['_id', 'firstName', 'lastName'] },
					{ path: 'updaterId', select: ['_id', 'firstName', 'lastName'] },
					{ path: 'target', select: ['_id', 'name'] },
				],
			},
		};
	}

	static decorateResults(result) {
		const decorate = n => ({
			...n,
			school: n.schoolId,
			schoolId: (n.schoolId || {})._id,
			creator: n.creatorId,
			creatorId: (n.creatorId || {})._id,
			updater: n.updaterId,
			updaterId: (n.updaterId || {})._id,
		});
		if (result instanceof Array) {
			return result.map(decorate);
		}
		if (result.data) { // paginated result set
			const dataIdsFixed = result.data.map(decorate);
			return { ...result, data: dataIdsFixed };
		}
		return decorate(result);
	}

	async decoratePermissions(result, userId) {
		// decorate permissions in data objects
		const decoratedData = await Promise.all(result.data.map(async n => ({
			...n,
			permissions: await this.getPermissions(userId, {
				target: (n.target || {})._id,
				targetModel: n.targetModel,
				schoolId: n.schoolId,
			}),
		})));
		return { ...result, data: decoratedData };
	}

	/**
	 * Create a copy of the original news if it is edited
	 * @param {News} oldItem
	 * @returns {Promise<NewsHistory>} the created news history document
	 * @memberof NewsService
	 * @static
	 */
	static createHistoryEntry(oldItem) {
		const historyEntry = {
			title: oldItem.title,
			content: oldItem.content,
			displayAt: oldItem.displayAt,
			creatorId: oldItem.updaterId ? oldItem.updaterId : oldItem.creatorId,
			parentId: oldItem._id,
		};
		return newsHistoryModel.create(historyEntry);
	}

	async buildFindQuery(params, baseFilter) {
		const query = [];
		const scoped = !!(params.query && (params.query.target || params.query.targetModel));

		if (scoped) {
			// add selected scope news
			query.push(await super.createScopedQuery(
				params.account.userId, baseFilter.permission, params.query.target, params.query.targetModel,
			));
		} else {
			// add school news
			query.push(await super.createSchoolQuery(
				params.account.userId, params.account.schoolId, baseFilter.permission,
			));
			if (params.query && params.query.target !== 'school') {
				// add all scope news if more than the current school is requested
				query.push(await super.createScopedQuery(
					params.account.userId, baseFilter.permission,
				));
			}
		}
		return flatten(query);
	}

	/**
	 * GET /news/{id}
	 * Returns the news item specified by id
	 * @param {BsonId|String} id
	 * @param {Object} params
	 * @returns one news item
	 * @throws {Forbidden} if not authorized
	 * @throws {NotFound} if the id does not belong to a news object
	 * @memberof NewsService
	 */
	async get(id, params) {
		const news = await this.app.service('newsModel').get(id, NewsService.populateParams());
		this.checkExistence(news, id);
		await this.authorize(news, params.account.userId, newsPermissions.VIEW);
		news.permissions = await this.getPermissions(params.account.userId, news);
		return NewsService.decorateResults(news);
	}

	/**
	 * GET /news/
	 * Returns all news the user can see.
	 * @param {Object} params
	 * @returns paginated array of news items
	 * @memberof NewsService
	 */
	async find(params) {
		const now = Date.now();
		// based on params.unpublished divide between view published news and unpublished news with edit permission
		const baseFilter = {
			published: params.unpublished ? { $gt: now } : { $lte: now },
			permission: params.unpublished ? newsPermissions.EDIT : newsPermissions.VIEW,
		};
		const query = {
			query: {
				displayAt: baseFilter.published,
				$or: await this.buildFindQuery(params, baseFilter),
				$sort: (params.query || {}).$sort,
				$limit: (params.query || {}).$limit,
				$skip: (params.query || {}).$skip,
				$populate: NewsService.populateParams().query.$populate,
			},
			$paginate: (params.query || {}).$paginate,
		};
		return this.app.service('newsModel')
			.find(query)
			.then(NewsService.decorateResults)
			.then(result => this.decoratePermissions(result, params.account.userId));
	}

	/**
	 * POST /news/
	 * Creates a news item
	 * @param {Object} data @see NewsModel
	 * @param {Object} params
	 * @returns {News} the created news object
	 * @throws {Forbidden} if not authorized
	 * @memberof NewsService
	 */
	async create(data, params) {
		await this.authorize(data, params.account.userId, newsPermissions.CREATE);
		const newNewsData = {
			...data,
			creatorId: params.account.userId,
			updaterId: null,
		};
		return this.app.service('newsModel').create(newNewsData);
	}

	/**
	 * DELETE /news/{id}
	 * Deletes a single news item.
	 * @param {BsonId|String} id The news item's Id
	 * @param {Object} params Note that params.query won't work here
	 * @returns {News} the deleted news item
	 * @throws {Forbidden} if not authorized
	 * @throws {NotFound} if the id does not belong to a news object
	 * @memberof NewsService
	 */
	async remove(id, params) {
		const news = await this.app.service('newsModel').get(id, NewsService.populateParams());
		this.checkExistence(news, id);
		await this.authorize(news, params.account.userId, newsPermissions.REMOVE);
		await this.app.service('newsModel').remove(id);
		return NewsService.decorateResults(news);
	}

	/**
	 * PUT /news/{id}
	 * Replaces a single news item
	 * @param {BsonId|String} id The news item's Id
	 * @param {News} data updated news item
	 * @param {Object} params Feathers request params (note that using params.query won't work here)
	 * @returns {News} updated news item
	 * @throws {Forbidden} if not authorized
	 * @throws {NotFound} if the id does not belong to a news object
	 * @memberof NewsService
	 */
	async update(id, data, params) {
		const news = await this.app.service('newsModel').get(id);
		this.checkExistence(news, id);
		await this.authorize(news, params.account.userId, newsPermissions.EDIT);
		const updatedNewsData = {
			...data,
			updaterId: params.account.userId,
		};
		const updatedNews = await this.app.service('newsModel').update(id, {
			...updatedNewsData, ...NewsService.populateParams(),
		});
		await NewsService.createHistoryEntry(news);
		return NewsService.decorateResults(updatedNews);
	}

	/**
	 * PATCH /news/{id}
	 * Patches a single news item
	 * @param {BsonId|String} id The news item's Id
	 * @param {Object} data object containing updated news item attributes
	 * @param {Object} params Feathers request params (note that using params.query won't work here)
	 * @returns {News} patched news item
	 * @throws {Forbidden} if not authorized
	 * @throws {NotFound} if the id does not belong to a news object
	 * @memberof NewsService
	 */
	async patch(id, data, params) {
		const news = await this.app.service('newsModel').get(id, NewsService.populateParams());
		this.checkExistence(news, id);
		await this.authorize(news, params.account.userId, newsPermissions.EDIT);
		const patchedNewsData = {
			...data,
			updaterId: params.account.userId,
		};
		const patchedNews = await this.app.service('newsModel').patch(id, {
			...patchedNewsData,
			...NewsService.populateParams(),
		});
		await NewsService.createHistoryEntry(news);
		return NewsService.decorateResults(patchedNews);
	}
}

module.exports = function news() {
	const app = this;

	// use /news to access a user's news
	app.use('/news', new NewsService());
	app.service('news').hooks(hooks);

	// use /newsModel to directly access the model from other services
	// (external requests are blocked)
	app.use('/newsModel', service({
		Model: newsModel,
		lean: true,
		paginate: {
			default: 25,
		},
	}));
	app.service('/newsModel').hooks(newsModelHooks);
};
<commit_msg>Handle unpublished query parameter<commit_after>const service = require('feathers-mongoose');
const { Forbidden, NotFound, BadRequest } = require('@feathersjs/errors');
const logger = require('winston');
const { ObjectId } = require('mongoose').Types;
const {
	newsModel, targetModels, newsHistoryModel, newsPermissions,
} = require('./model');
const hooks = require('./hooks');
const newsModelHooks = require('./hooks/newsModel.hooks');
const { flatten } = require('../../utils/array');

class AbstractService {
	setup(app) {
		this.app = app;
	}

	/**
	* Returns school query if the user is allowed to see.
	* @param userId The user's Id
	* @param schoolId The schoolId
	* @returns Query
	*/
	createSchoolQuery(userId, schoolId, permission) {
		return this.hasSchoolPermission(userId, schoolId, permission)
			.then((hasPermission) => {
				if (!hasPermission) {
					return null;
				}
				return [{
					schoolId,
					target: { $exists: false },
				}];
			});
	}

	/**
		 * Returns scoped news the user is allowed to see
		 *
		 * @param {BsonId|String} userId the user's Id
		 * @param {BsonId|String} target (optional) Id of the news target (course, team, etc.)
		 * @returns Array<Query>
		 */
	async createScopedQuery(userId, permission, target = null, targetModel = null) {
		// check params
		if (target ? !targetModel : targetModel) {
			throw new BadRequest('target and targetModel, both must be given or not');
		}
		// only one target requested
		if (target && targetModel) {
			return this.hasPermission(userId, permission, { target, targetModel })
				.then(() => (
					[{
						targetModel,
						target,
					}]));
		}
		// return data of all user scopes
		const ops = targetModels.map(async (scope) => {
			// For each possible target model, find all targets the user has NEWS_VIEW permissions in.
			const scopeListService = this.app.service(`/users/:scopeId/${scope}`);
			if (scopeListService === undefined) {
				return null;
			}
			const scopeItems = await scopeListService.find({
				route: { scopeId: userId.toString() },
				query: {
					permissions: [permission],
				},
			});
			return scopeItems.map(item => ({
				targetModel: scope,
				target: item._id,
			}));
		});
		const results = await Promise.all(ops);
		return flatten(results.filter(r => r !== null));
	}

	/* Permissions */

	async getPermissions(userId, { target, targetModel, schoolId } = {}) {
		// target and school might be populated or not
		const isObjectId = o => o instanceof ObjectId || typeof o === 'string';
		// scope case: user role in scope must have given permission
		if (target && targetModel) {
			const targetId = isObjectId(target) ? target.toString() : target._id.toString();
			const scope = this.app.service(`${targetModel}/:scopeId/userPermissions/`);
			const params = { route: { scopeId: targetId } };
			const scopePermissions = await scope.get(userId, params);
			return scopePermissions;
		}

		// default school case: dataItem and users schoolId must match and user permission must exist
		return this.getSchoolPermissions(userId, isObjectId(schoolId) ? schoolId : schoolId._id);
	}

	/**
	 * Checks scoped permission for a user and given news.
	 * @param {BsonId|String} userId
	 * @param {String} permission
	 * @param {BsonId|String} targetId (optional) news target (scope) id. Only valid with targetModel.
	 * @param {String} targetModel (optional) news target (scope) model. Only valid with target.
	 * @returns {Promise<Boolean>} Promise that resolves to true/false
	 * @example
	 * await hasPermission(user._id, 'NEWS_VIEW') => true
	 * await hasPermission(user._id, 'NEWS_CREATE', team._id, 'teams') => false
	 * @memberof NewsService
	 */
	async hasPermission(userId, permission, dataItem) {
		if (!dataItem) {
			// maybe, use hasSchoolPermission instead...
			return false;
		}
		const permissions = await this.getPermissions(userId, dataItem);
		return permissions.includes(permission);
	}

	/**
	 * Tests if a user with given userId has a (global) permission within a school.
	 * @param {ObjectId} userId
	 * @param {ObjectId} schoolId
	 * @returns {Array<String>}
	 */
	async getSchoolPermissions(userId, schoolId) {
		// test user exists
		const user = await this.app.service('users').get(userId);
		if (user == null) return [];

		// test user is school member
		const sameSchool = schoolId.toString() === user.schoolId.toString();
		if (!sameSchool) return [];

		return user.permissions;
	}

	async hasSchoolPermission(userId, schoolId, permision) {
		const permissions = await this.getSchoolPermissions(userId, schoolId);
		return permissions.includes(permision);
	}

	/**
	 * Throw an error if the user is not allowed to perform the given operation
	 * @param {News} news news item (required: {schoolId}, optional: {target, targetModel})
	 * @param {Object} { userId, schoolId }
	 * @param {String} permission permision to check
	 * @returns undefined
	 * @example
	 * await authorize(news, params.account, 'NEWS_VIEW') => undefined
	 * await authorize(news, params.account, 'NEWS_CREATE') => (throws Forbidden)
	 * @memberof NewsService
	 */
	async authorize(news = {}, userId, permission) {
		const authorized = await this.hasPermission(userId, permission, news);
		if (!authorized) {
			throw new Forbidden('Not authorized.');
		}
	}

	checkExistence(news, query) {
		if (!news) {
			logger.error(`Cannot find news item with query "${query}" => "${news}"`);
			throw new NotFound('News item does not exist.');
		}
	}
}

class NewsService extends AbstractService {
	static populateParams() {
		return {
			query: {
				$populate: [
					{ path: 'schoolId', select: ['_id', 'name'] },
					{ path: 'creatorId', select: ['_id', 'firstName', 'lastName'] },
					{ path: 'updaterId', select: ['_id', 'firstName', 'lastName'] },
					{ path: 'target', select: ['_id', 'name'] },
				],
			},
		};
	}

	static decorateResults(result) {
		const decorate = n => ({
			...n,
			school: n.schoolId,
			schoolId: (n.schoolId || {})._id,
			creator: n.creatorId,
			creatorId: (n.creatorId || {})._id,
			updater: n.updaterId,
			updaterId: (n.updaterId || {})._id,
		});
		if (result instanceof Array) {
			return result.map(decorate);
		}
		if (result.data) { // paginated result set
			const dataIdsFixed = result.data.map(decorate);
			return { ...result, data: dataIdsFixed };
		}
		return decorate(result);
	}

	async decoratePermissions(result, userId) {
		// decorate permissions in data objects
		const decoratedData = await Promise.all(result.data.map(async n => ({
			...n,
			permissions: await this.getPermissions(userId, {
				target: (n.target || {})._id,
				targetModel: n.targetModel,
				schoolId: n.schoolId,
			}),
		})));
		return { ...result, data: decoratedData };
	}

	/**
	 * Create a copy of the original news if it is edited
	 * @param {News} oldItem
	 * @returns {Promise<NewsHistory>} the created news history document
	 * @memberof NewsService
	 * @static
	 */
	static createHistoryEntry(oldItem) {
		const historyEntry = {
			title: oldItem.title,
			content: oldItem.content,
			displayAt: oldItem.displayAt,
			creatorId: oldItem.updaterId ? oldItem.updaterId : oldItem.creatorId,
			parentId: oldItem._id,
		};
		return newsHistoryModel.create(historyEntry);
	}

	async buildFindQuery(params, baseFilter) {
		const query = [];
		const scoped = !!(params.query && (params.query.target || params.query.targetModel));

		if (scoped) {
			// add selected scope news
			query.push(await super.createScopedQuery(
				params.account.userId, baseFilter.permission, params.query.target, params.query.targetModel,
			));
		} else {
			// add school news
			query.push(await super.createSchoolQuery(
				params.account.userId, params.account.schoolId, baseFilter.permission,
			));
			if (params.query && params.query.target !== 'school') {
				// add all scope news if more than the current school is requested
				query.push(await super.createScopedQuery(
					params.account.userId, baseFilter.permission,
				));
			}
		}
		return flatten(query);
	}

	/**
	 * GET /news/{id}
	 * Returns the news item specified by id
	 * @param {BsonId|String} id
	 * @param {Object} params
	 * @returns one news item
	 * @throws {Forbidden} if not authorized
	 * @throws {NotFound} if the id does not belong to a news object
	 * @memberof NewsService
	 */
	async get(id, params) {
		const news = await this.app.service('newsModel').get(id, NewsService.populateParams());
		this.checkExistence(news, id);
		await this.authorize(news, params.account.userId, newsPermissions.VIEW);
		news.permissions = await this.getPermissions(params.account.userId, news);
		return NewsService.decorateResults(news);
	}

	/**
	 * GET /news/
	 * Returns all news the user can see.
	 * @param {Object} params
	 * @returns paginated array of news items
	 * @memberof NewsService
	 */
	async find(params) {
		const now = Date.now();
		// based on params.unpublished divide between view published news and unpublished news with edit permission
		const baseFilter = {
			published: (params.query || {}).unpublished ? { $gt: now } : { $lte: now },
			permission: (params.query || {}).unpublished ? newsPermissions.EDIT : newsPermissions.VIEW,
		};
		const query = {
			query: {
				displayAt: baseFilter.published,
				$or: await this.buildFindQuery(params, baseFilter),
				$sort: (params.query || {}).$sort,
				$limit: (params.query || {}).$limit,
				$skip: (params.query || {}).$skip,
				$populate: NewsService.populateParams().query.$populate,
			},
			$paginate: (params.query || {}).$paginate,
		};
		return this.app.service('newsModel')
			.find(query)
			.then(NewsService.decorateResults)
			.then(result => this.decoratePermissions(result, params.account.userId));
	}

	/**
	 * POST /news/
	 * Creates a news item
	 * @param {Object} data @see NewsModel
	 * @param {Object} params
	 * @returns {News} the created news object
	 * @throws {Forbidden} if not authorized
	 * @memberof NewsService
	 */
	async create(data, params) {
		await this.authorize(data, params.account.userId, newsPermissions.CREATE);
		const newNewsData = {
			...data,
			creatorId: params.account.userId,
			updaterId: null,
		};
		return this.app.service('newsModel').create(newNewsData);
	}

	/**
	 * DELETE /news/{id}
	 * Deletes a single news item.
	 * @param {BsonId|String} id The news item's Id
	 * @param {Object} params Note that params.query won't work here
	 * @returns {News} the deleted news item
	 * @throws {Forbidden} if not authorized
	 * @throws {NotFound} if the id does not belong to a news object
	 * @memberof NewsService
	 */
	async remove(id, params) {
		const news = await this.app.service('newsModel').get(id, NewsService.populateParams());
		this.checkExistence(news, id);
		await this.authorize(news, params.account.userId, newsPermissions.REMOVE);
		await this.app.service('newsModel').remove(id);
		return NewsService.decorateResults(news);
	}

	/**
	 * PUT /news/{id}
	 * Replaces a single news item
	 * @param {BsonId|String} id The news item's Id
	 * @param {News} data updated news item
	 * @param {Object} params Feathers request params (note that using params.query won't work here)
	 * @returns {News} updated news item
	 * @throws {Forbidden} if not authorized
	 * @throws {NotFound} if the id does not belong to a news object
	 * @memberof NewsService
	 */
	async update(id, data, params) {
		const news = await this.app.service('newsModel').get(id);
		this.checkExistence(news, id);
		await this.authorize(news, params.account.userId, newsPermissions.EDIT);
		const updatedNewsData = {
			...data,
			updaterId: params.account.userId,
		};
		const updatedNews = await this.app.service('newsModel').update(id, {
			...updatedNewsData, ...NewsService.populateParams(),
		});
		await NewsService.createHistoryEntry(news);
		return NewsService.decorateResults(updatedNews);
	}

	/**
	 * PATCH /news/{id}
	 * Patches a single news item
	 * @param {BsonId|String} id The news item's Id
	 * @param {Object} data object containing updated news item attributes
	 * @param {Object} params Feathers request params (note that using params.query won't work here)
	 * @returns {News} patched news item
	 * @throws {Forbidden} if not authorized
	 * @throws {NotFound} if the id does not belong to a news object
	 * @memberof NewsService
	 */
	async patch(id, data, params) {
		const news = await this.app.service('newsModel').get(id, NewsService.populateParams());
		this.checkExistence(news, id);
		await this.authorize(news, params.account.userId, newsPermissions.EDIT);
		const patchedNewsData = {
			...data,
			updaterId: params.account.userId,
		};
		const patchedNews = await this.app.service('newsModel').patch(id, {
			...patchedNewsData,
			...NewsService.populateParams(),
		});
		await NewsService.createHistoryEntry(news);
		return NewsService.decorateResults(patchedNews);
	}
}

module.exports = function news() {
	const app = this;

	// use /news to access a user's news
	app.use('/news', new NewsService());
	app.service('news').hooks(hooks);

	// use /newsModel to directly access the model from other services
	// (external requests are blocked)
	app.use('/newsModel', service({
		Model: newsModel,
		lean: true,
		paginate: {
			default: 25,
		},
	}));
	app.service('/newsModel').hooks(newsModelHooks);
};
<|endoftext|>


<commit_before>let files = new Map();

export default class Loader {
  static loadImages(urls) {
    let promises = [];
    for(let url of urls) {
      promises.push(Loader.loadImage(url));
    }
    return Promise.all(promises);
  }
  static loadImage(url) {
    let image = files.get(url);
    if(image) {
      return Promise.resolve(image);
    }
    return new Promise(function(resolve, reject) {
      image = new Image();
      image.onload = () => {
        resolve(image);
      };
      image.src = url;
    });
  }
}
<commit_msg>add crossorigin option to Loader<commit_after>let files = new Map();

export default class Loader {
  static loadImages(urls, options) {
    let promises = [];
    for(let url of urls) {
      promises.push(Loader.loadImage(url, options));
    }
    return Promise.all(promises);
  }
  static loadImage(url, {crossOrigin = null} = {}) {
    let image = files.get(url);
    if(image) {
      return Promise.resolve(image);
    }
    return new Promise(function(resolve, reject) {
      image = new Image();
      image.crossOrigin = crossOrigin;
      image.onload = () => {
        resolve(image);
      };
      image.src = url;
    });
  }
}
<|endoftext|>


<commit_before>var cheerio = require('cheerio');
var parserUtil = require('./parserUtil');

module.exports.parse = function(html, callback) {

    var $ = cheerio.load(html);

    var menuText = $('div.entry-content').first().text().trim().split(/\n/).map(function(line) {
        return line.replace(/\t/g, '');
    });

    var weekMenu = [];

    global.dates.forEach(function(date) {
        var dayMenu = parseDailyMenu(menuText, date);
        if(!dayMenu || dayMenu.length === 0) {
            callback(dayMenu);
            return;
        }

        //the first menu entry is also a soup
        dayMenu[0] = dayMenu[0] === '' ? 'Dnes v menu chýba polievka' : dayMenu[0];

        dayMenu = dayMenu.map(function(line) {
            //prize is written the other way around than usual e.g. € 5.10 instead of 5.10 €
            return line.replace(/€ ([0-9]{1,2},[0-9]{2})/, '$1 €');
        });

        //sometimes one menu item is scattered across multiple lines of HTML
        //lines with extra indentation should be merged with previous line
        for(var i = 0; i < dayMenu.length; i++) {
            if(/^\s{4,}/.test(dayMenu[i])) {
                dayMenu[i - 1] = dayMenu[i - 1] + " " + dayMenu[i].trim();
                dayMenu.splice(i, 1);
                i--;
            }
            else if(/\s*menu č\. ?4/i.test(dayMenu[i])) {
                if(i + 1 < dayMenu.length) //prepend it to the next item
                    dayMenu[i + 1] = dayMenu[i].trim() + ": " + dayMenu[i + 1].trim();
                dayMenu.splice(i, 1);
            }
            else {
                dayMenu[i] = dayMenu[i].trim();
            }
        }

        //convert to objects
        dayMenu = dayMenu.map(function(item, index) {
            var priced = parserUtil.parsePrice(item);
            return { isSoup: index === 0, text: normalize(priced.text), price: priced.price };
        });
        weekMenu.push({ day: date.format('dddd'), menu: dayMenu });
    });

    callback(weekMenu);

    function parseDailyMenu(menuText, date) {
        var todayName = date.format("dddd");
        var tomorrowName = date.clone().add("days", 1).format("dddd");

        var startLine, endLine;
        for(var line in menuText) {
            if(menuText[line].toLowerCase().indexOf(todayName) !== -1) {
                startLine = line;
            }
            if(startLine && /^–+$/.test(menuText[line].trim()) //dashed separator line
                || (menuText[line].toLowerCase().indexOf(tomorrowName) !== -1)) //next day name
            {
                endLine = line;
                break;
            }
        }
        if(!startLine || (startLine >= endLine)) {
            return [];
        }

        var menuResult = menuText.slice(startLine, endLine);
        menuResult = menuResult.filter(function(line) { return /\S/.test(line); }); //remove empty lines
        menuResult = menuResult.filter(function(line) { return !/(Výmena prílohy|Pri účte 30 eur)/.test(line); }); //remove bonus information
        //remove name of the day from the first menu entry
        menuResult[0] = menuResult[0].substring(todayName.length).trim();
        return menuResult;
    }

    function normalize(str) {
        return str.normalizeWhitespace()
            .removeItemNumbering()
            .removeMetrics()
            .correctCommaSpacing();
    }
};

<commit_msg>jshint error<commit_after>var cheerio = require('cheerio');
var parserUtil = require('./parserUtil');

module.exports.parse = function(html, callback) {

    var $ = cheerio.load(html);

    var menuText = $('div.entry-content').first().text().trim().split(/\n/).map(function(line) {
        return line.replace(/\t/g, '');
    });

    var weekMenu = [];

    global.dates.forEach(function(date) {
        var dayMenu = parseDailyMenu(menuText, date);
        if(!dayMenu || dayMenu.length === 0) {
            callback(dayMenu);
            return;
        }

        //the first menu entry is also a soup
        dayMenu[0] = dayMenu[0] === '' ? 'Dnes v menu chýba polievka' : dayMenu[0];

        dayMenu = dayMenu.map(function(line) {
            //prize is written the other way around than usual e.g. € 5.10 instead of 5.10 €
            return line.replace(/€ ([0-9]{1,2},[0-9]{2})/, '$1 €');
        });

        //sometimes one menu item is scattered across multiple lines of HTML
        //lines with extra indentation should be merged with previous line
        for(var i = 0; i < dayMenu.length; i++) {
            if(/^\s{4,}/.test(dayMenu[i])) {
                dayMenu[i - 1] = dayMenu[i - 1] + " " + dayMenu[i].trim();
                dayMenu.splice(i, 1);
                i--;
            }
            else if(/\s*menu č\. ?4/i.test(dayMenu[i])) {
                if(i + 1 < dayMenu.length) //prepend it to the next item
                    dayMenu[i + 1] = dayMenu[i].trim() + ": " + dayMenu[i + 1].trim();
                dayMenu.splice(i, 1);
            }
            else {
                dayMenu[i] = dayMenu[i].trim();
            }
        }

        //convert to objects
        dayMenu = dayMenu.map(function(item, index) {
            var priced = parserUtil.parsePrice(item);
            return { isSoup: index === 0, text: normalize(priced.text), price: priced.price };
        });
        weekMenu.push({ day: date.format('dddd'), menu: dayMenu });
    });

    callback(weekMenu);

    function parseDailyMenu(menuText, date) {
        var todayName = date.format("dddd");
        var tomorrowName = date.clone().add("days", 1).format("dddd");

        var startLine, endLine;
        for(var line in menuText) {
            if(menuText[line].toLowerCase().indexOf(todayName) !== -1) {
                startLine = line;
            }
            if(startLine && /^–+$/.test(menuText[line].trim()) || //dashed separator line
                (menuText[line].toLowerCase().indexOf(tomorrowName) !== -1)) //next day name
            {
                endLine = line;
                break;
            }
        }
        if(!startLine || (startLine >= endLine)) {
            return [];
        }

        var menuResult = menuText.slice(startLine, endLine);
        menuResult = menuResult.filter(function(line) { return /\S/.test(line); }); //remove empty lines
        menuResult = menuResult.filter(function(line) { return !/(Výmena prílohy|Pri účte 30 eur)/.test(line); }); //remove bonus information
        //remove name of the day from the first menu entry
        menuResult[0] = menuResult[0].substring(todayName.length).trim();
        return menuResult;
    }

    function normalize(str) {
        return str.normalizeWhitespace()
            .removeItemNumbering()
            .removeMetrics()
            .correctCommaSpacing();
    }
};

<|endoftext|>


<commit_before>var async   = require('async');
var request = require('request'); // github.com/mikeal/request
var formidable = require('formidable');
var crypto = require('crypto');

module.exports = function(app, io, slack, models) {

    var application = null;

    function isAuthorized(req, res, next) {

        var appid = req.param('appid');

        if(appid === null) {
            return res.status(401).json({ success: false, message: "Unauthorized" });
        }

        models.app.find({ where: { appid: appid } }).success(function(app) {
            if(app === null) {
                return res.status(401).json({ success: false, message: "Unauthorized" });
            }

            if(app.online === false) {
                return res.status(503).json({ success: false, message: "Support offline" });
            }

            if(app.active === false) {
                return res.status(404).json({ success: false, message: "Application offline" });
            }

            application = app;

            return next();
        });
    }

    function isAdmin(req, res, next) {

        var appid = req.param('appid');
        var token = req.param('token'); // Private token

        if(appid === null || token === null) {
            return res.status(401).json({ success: false, message: "Unauthorized" });
        }

        // TODO: use the real token
        models.app.find({ where: { appid: appid, token: slack_api_token } }).success(function(app) {
            if(app === null) {
                return res.status(401).json({ success: false, message: "Unauthorized" });
            }

            if(app.online === false) {
                return res.status(503).json({ success: false, message: "Support offline" });
            }

            if(app.active === false) {
                return res.status(404).json({ success: false, message: "Application offline" });
            }

            application = app;

            return next();
        });
    }

    app.get('/', function(req, res, next) {
        return res.status(200).json({ success: true, message: "Welcome, nothing here" });
    });

    app.post('/app/file-upload', isAuthorized, function(req, res, next) {

        // Retrieve Prudio appid
        var appid       = req.param('appid');

        // Retrieve Slack channel
        var channel     = req.param('channel');

        // Create the POST parser object
        var form        = new formidable.IncomingForm();

        // Parse data from POST
        form.parse(req, function(err, fields, files) {
            models.app.find({ where: { appid: appid, active: true } }).success(function(application) {
                slack.uploadFile(application.slack_api_token,channel, files);
                return res.status(200).json({ success: true, message: "Uploading" });
            });
        });

    });

    // TODO: Use diferent method for authorization
    app.post('/app/connect', isAdmin, function(req, res, next) {
        var appid            = req.param('appid');
        models.app.find({ where: { appid: appid, active: true } }).success(function(application) {
            slack.connect(application);

            return res.status(200).json({ success: true, message: "Initializing" });
        });
    });

    // TODO: Use diferent method for authorization
    app.post('/app/disconnect', isAdmin, function(req, res, next) {
        var appid            = req.param('appid');
        models.app.find({ where: { appid: appid, active: true } }).success(function(application) {
            slack.disconnect(application.id);

            return res.status(200).json({ success: true, message: "Disconnecting" });
        });
    });

    app.post('/app/ping', isAuthorized, function(req, res, next) {
        var appid            = req.param('appid');
        models.app.find({ where: { appid: appid, active: true } }).success(function(application) {
            var onlineUsers = slack.onlineUsers(application.id);

            return res.status(200).json({ success: true, onlineUsers: onlineUsers.length, message: onlineUsers.length + " users online." });
        });
    });

    app.post('/chat/history', isAuthorized, function(req, res, next) {

        var appid            = req.param('appid');
        var channel          = req.param('channel');
        var channelSignature = req.param('signature');

        async.waterfall(
            [
                function(callback) {
                    models.app.find({ where: { appid: appid, active: true } }).success(function(application) {
                        return callback(null, application);
                    });
                },

                function(application, callback) {
                    // Verify if the user already has previous support (from cookies)
                    if(channel !== null && channelSignature !== null) {
                        // Returning user with cookie
                        var verify = crypto.createHmac('sha1', application.slack_api_token).update(channel).digest('hex');

                        // Verify signature else it will create a new one!
                        if(verify === channelSignature) {
                            return callback(null, application);
                        } else {
                            return callback('Could not verify signature');
                        }
                    }
                },

                function(application, callback) {
                    return slack.history(application, channel, callback);
                }
            ],

            function(err, body, application) {
                if (err) {
                    return res.status(404).json({ success: false, result: err });
                }

                body.messages.reverse();

                var messages = [];

                for (var i in body.messages) {
                    if (body.messages.hasOwnProperty(i)) {
                        var message = body.messages[i];
                        if (message.type === 'message' && typeof message.subtype === 'undefined') {
                            console.log('Channel message: %j', message);

                            if (message.user === application.slack_invite_bot) {
                                message.sender = 'self';
                            } else {
                                message.sender = 'other';
                            }

                            message.user = undefined;
                            message.type = undefined;

                            messages.push(message);
                        }
                    }
                }

                return res.status(200).json({ success: true, messages: messages, messagesCount: messages.length });
            }
        );

    });

    app.post('/chat/create', isAuthorized, function(req, res, next) {

        var appid            = req.param('appid');
        var channelId        = req.param('channel');
        var channelName      = req.param('channelName');
        var channelSignature = req.param('signature');
        var userInfo         = req.param('userInfo');
        var settings         = req.param('settings');

        models.app.find({ where: { appid: appid, active: true } }).success(function(application) {

            if(application === null) {
                return res.status(404).json({ success: false, message: "Not found" });
            }

            async.waterfall(
                [
                    function(callback) {

                        // Verify if the user already has previous support (from cookies)
                        if(channelId !== null && channelSignature !== null) {
                            // Returning user with cookie
                            var verify = crypto.createHmac('sha1', application.slack_api_token).update(channelId).digest('hex');

                            // Verify signature else it will create a new one!
                            if(verify === channelSignature) {
                                return callback(null, channelName, channelId);
                            }
                        }

                        // No channel or signature, or invalid signature/channel, get the next channel
                        application.increment('room_count').success(function() {
                            var channelName = application.room_prefix + application.room_count;
                            return callback(null, channelName, null);
                        });

                    },

                    // Create channel
                    function(channelName, channelId, callback) {

                        if(channelId !== null) {
                            return callback(null, channelName, channelId, true)
                        } else {

                            request.post(app.get('slack_api_url') + '/channels.join', { json: true, form: { token: application.slack_api_token, name: channelName }}, function (error, response, body) {
                                if (!error && response.statusCode === 200 && typeof body.channel !== "undefined") {
                                    return callback(null, channelName, body.channel.id, false);
                                }

                                return callback('Create Channel');
                            });
                        }
                    },

                    // Invite user to channel
                    function(channelName, channelId, returning, callback) {
                        request.post(app.get('slack_api_url') + '/channels.invite', { json: true, form: { token: application.slack_api_token, channel: channelId, user: application.slack_invite_bot }}, function (error, response, body) {
                            if (!error && response.statusCode === 200) {
                                return callback(null, channelName, channelId, returning);
                            }
                            return callback('Invite user to channel');
                        });
                    },

                    // Set purpose of channel
                    function(channelName, channelId, returning, callback) {

                        if(returning) {
                            return callback(null, channelName, channelId);
                        }

                        var info     = JSON.parse(userInfo);
                        var personal = JSON.parse(settings);

                        var topic = "Help!" +
                        "\nName: " + personal.name + " (" + personal.email + ")" +
                        "\nURL: " + info.url + " (" + req.ip + ")" +
                        "\nBrowser: " + info.browser + " - " + info.browserVersion +
                        "\nOS: " + info.os + " - " + info.osVersion +
                        "\nMobile: " + info.mobile +" - Screen resolution: " + info.screen;

                        request.post(app.get('slack_api_url') + '/channels.setPurpose', { json: true, form: { token: application.slack_api_token, channel: channelId, purpose: topic }}, function (error, response, body) {
                            if (!error && response.statusCode === 200) {
                                return callback(null, channelName, channelId);
                            }

                            return callback('Set purpose of channel');
                        });
                    },

                    // Leave the channel
                    function(channelName, channelId, callback) {

                        if (!application.join) {
                            request.post(app.get('slack_api_url') + '/channels.leave', { json: true, form: { token: application.slack_api_token, channel: channelId }}, function (error, response, body) {
                                if (!error && response.statusCode === 200) {
                                    return callback(null, channelName, channelId);
                                }

                                return callback('Leave the channel');
                            });
                        } else {
                            return callback(null, channelName, channelId);
                        }
                    },

                    // Send notification
                    function(channelName, channelId, callback) {

                        var text = "New help request at channel <#" + channelId + "|" + channelName + ">. Join now!";

                        request.post(app.get('slack_api_url') + '/chat.postMessage', { json: true, form: { token: application.slack_api_token, channel: '#general', text: text, username: 'Prud.io', icon_url: 'http://chat.prud.io/prudio-notification-icon.png' }}, function (error, response, body) {
                            if (!error && response.statusCode === 200) {
                                return callback(null, channelName, channelId);
                            }
                            return callback('Leave the channel');
                        });
                    },

                ],
                function(err, channelName, channelId) {
                    if(err) {
                        console.log("ERROR: " + err);
                        return res.status(404).json({ error: "Error: " + err});
                    }

                    var signature = crypto.createHmac('sha1', application.slack_api_token).update(channelId).digest('hex');

                    return res.status(200).json({ success: true, channel: channelId, channelName: channelName, signature: signature });
                }
            );
        });

    });

};
<commit_msg>Token issue<commit_after>var async   = require('async');
var request = require('request'); // github.com/mikeal/request
var formidable = require('formidable');
var crypto = require('crypto');

module.exports = function(app, io, slack, models) {

    var application = null;

    function isAuthorized(req, res, next) {

        var appid = req.param('appid');

        if(appid === null) {
            return res.status(401).json({ success: false, message: "Unauthorized" });
        }

        models.app.find({ where: { appid: appid } }).success(function(app) {
            if(app === null) {
                return res.status(401).json({ success: false, message: "Unauthorized" });
            }

            if(app.online === false) {
                return res.status(503).json({ success: false, message: "Support offline" });
            }

            if(app.active === false) {
                return res.status(404).json({ success: false, message: "Application offline" });
            }

            application = app;

            return next();
        });
    }

    function isAdmin(req, res, next) {

        var appid = req.param('appid');
        var token = req.param('token'); // Private token

        if(appid === null || token === null) {
            return res.status(401).json({ success: false, message: "Unauthorized" });
        }

        // TODO: use the real token
        models.app.find({ where: { appid: appid, slack_api_token: token } }).success(function(app) {
            if(app === null) {
                return res.status(401).json({ success: false, message: "Unauthorized" });
            }

            if(app.online === false) {
                return res.status(503).json({ success: false, message: "Support offline" });
            }

            if(app.active === false) {
                return res.status(404).json({ success: false, message: "Application offline" });
            }

            application = app;

            return next();
        });
    }

    app.get('/', function(req, res, next) {
        return res.status(200).json({ success: true, message: "Welcome, nothing here" });
    });

    app.post('/app/file-upload', isAuthorized, function(req, res, next) {

        // Retrieve Prudio appid
        var appid       = req.param('appid');

        // Retrieve Slack channel
        var channel     = req.param('channel');

        // Create the POST parser object
        var form        = new formidable.IncomingForm();

        // Parse data from POST
        form.parse(req, function(err, fields, files) {
            models.app.find({ where: { appid: appid, active: true } }).success(function(application) {
                slack.uploadFile(application.slack_api_token,channel, files);
                return res.status(200).json({ success: true, message: "Uploading" });
            });
        });

    });

    // TODO: Use diferent method for authorization
    app.post('/app/connect', isAdmin, function(req, res, next) {
        var appid            = req.param('appid');
        models.app.find({ where: { appid: appid, active: true } }).success(function(application) {
            slack.connect(application);

            return res.status(200).json({ success: true, message: "Initializing" });
        });
    });

    // TODO: Use diferent method for authorization
    app.post('/app/disconnect', isAdmin, function(req, res, next) {
        var appid            = req.param('appid');
        models.app.find({ where: { appid: appid, active: true } }).success(function(application) {
            slack.disconnect(application.id);

            return res.status(200).json({ success: true, message: "Disconnecting" });
        });
    });

    app.post('/app/ping', isAuthorized, function(req, res, next) {
        var appid            = req.param('appid');
        models.app.find({ where: { appid: appid, active: true } }).success(function(application) {
            var onlineUsers = slack.onlineUsers(application.id);

            return res.status(200).json({ success: true, onlineUsers: onlineUsers.length, message: onlineUsers.length + " users online." });
        });
    });

    app.post('/chat/history', isAuthorized, function(req, res, next) {

        var appid            = req.param('appid');
        var channel          = req.param('channel');
        var channelSignature = req.param('signature');

        async.waterfall(
            [
                function(callback) {
                    models.app.find({ where: { appid: appid, active: true } }).success(function(application) {
                        return callback(null, application);
                    });
                },

                function(application, callback) {
                    // Verify if the user already has previous support (from cookies)
                    if(channel !== null && channelSignature !== null) {
                        // Returning user with cookie
                        var verify = crypto.createHmac('sha1', application.slack_api_token).update(channel).digest('hex');

                        // Verify signature else it will create a new one!
                        if(verify === channelSignature) {
                            return callback(null, application);
                        } else {
                            return callback('Could not verify signature');
                        }
                    }
                },

                function(application, callback) {
                    return slack.history(application, channel, callback);
                }
            ],

            function(err, body, application) {
                if (err) {
                    return res.status(404).json({ success: false, result: err });
                }

                body.messages.reverse();

                var messages = [];

                for (var i in body.messages) {
                    if (body.messages.hasOwnProperty(i)) {
                        var message = body.messages[i];
                        if (message.type === 'message' && typeof message.subtype === 'undefined') {
                            console.log('Channel message: %j', message);

                            if (message.user === application.slack_invite_bot) {
                                message.sender = 'self';
                            } else {
                                message.sender = 'other';
                            }

                            message.user = undefined;
                            message.type = undefined;

                            messages.push(message);
                        }
                    }
                }

                return res.status(200).json({ success: true, messages: messages, messagesCount: messages.length });
            }
        );

    });

    app.post('/chat/create', isAuthorized, function(req, res, next) {

        var appid            = req.param('appid');
        var channelId        = req.param('channel');
        var channelName      = req.param('channelName');
        var channelSignature = req.param('signature');
        var userInfo         = req.param('userInfo');
        var settings         = req.param('settings');

        models.app.find({ where: { appid: appid, active: true } }).success(function(application) {

            if(application === null) {
                return res.status(404).json({ success: false, message: "Not found" });
            }

            async.waterfall(
                [
                    function(callback) {

                        // Verify if the user already has previous support (from cookies)
                        if(channelId !== null && channelSignature !== null) {
                            // Returning user with cookie
                            var verify = crypto.createHmac('sha1', application.slack_api_token).update(channelId).digest('hex');

                            // Verify signature else it will create a new one!
                            if(verify === channelSignature) {
                                return callback(null, channelName, channelId);
                            }
                        }

                        // No channel or signature, or invalid signature/channel, get the next channel
                        application.increment('room_count').success(function() {
                            var channelName = application.room_prefix + application.room_count;
                            return callback(null, channelName, null);
                        });

                    },

                    // Create channel
                    function(channelName, channelId, callback) {

                        if(channelId !== null) {
                            return callback(null, channelName, channelId, true)
                        } else {

                            request.post(app.get('slack_api_url') + '/channels.join', { json: true, form: { token: application.slack_api_token, name: channelName }}, function (error, response, body) {
                                if (!error && response.statusCode === 200 && typeof body.channel !== "undefined") {
                                    return callback(null, channelName, body.channel.id, false);
                                }

                                return callback('Create Channel');
                            });
                        }
                    },

                    // Invite user to channel
                    function(channelName, channelId, returning, callback) {
                        request.post(app.get('slack_api_url') + '/channels.invite', { json: true, form: { token: application.slack_api_token, channel: channelId, user: application.slack_invite_bot }}, function (error, response, body) {
                            if (!error && response.statusCode === 200) {
                                return callback(null, channelName, channelId, returning);
                            }
                            return callback('Invite user to channel');
                        });
                    },

                    // Set purpose of channel
                    function(channelName, channelId, returning, callback) {

                        if(returning) {
                            return callback(null, channelName, channelId);
                        }

                        var info     = JSON.parse(userInfo);
                        var personal = JSON.parse(settings);

                        var topic = "Help!" +
                        "\nName: " + personal.name + " (" + personal.email + ")" +
                        "\nURL: " + info.url + " (" + req.ip + ")" +
                        "\nBrowser: " + info.browser + " - " + info.browserVersion +
                        "\nOS: " + info.os + " - " + info.osVersion +
                        "\nMobile: " + info.mobile +" - Screen resolution: " + info.screen;

                        request.post(app.get('slack_api_url') + '/channels.setPurpose', { json: true, form: { token: application.slack_api_token, channel: channelId, purpose: topic }}, function (error, response, body) {
                            if (!error && response.statusCode === 200) {
                                return callback(null, channelName, channelId);
                            }

                            return callback('Set purpose of channel');
                        });
                    },

                    // Leave the channel
                    function(channelName, channelId, callback) {

                        if (!application.join) {
                            request.post(app.get('slack_api_url') + '/channels.leave', { json: true, form: { token: application.slack_api_token, channel: channelId }}, function (error, response, body) {
                                if (!error && response.statusCode === 200) {
                                    return callback(null, channelName, channelId);
                                }

                                return callback('Leave the channel');
                            });
                        } else {
                            return callback(null, channelName, channelId);
                        }
                    },

                    // Send notification
                    function(channelName, channelId, callback) {

                        var text = "New help request at channel <#" + channelId + "|" + channelName + ">. Join now!";

                        request.post(app.get('slack_api_url') + '/chat.postMessage', { json: true, form: { token: application.slack_api_token, channel: '#general', text: text, username: 'Prud.io', icon_url: 'http://chat.prud.io/prudio-notification-icon.png' }}, function (error, response, body) {
                            if (!error && response.statusCode === 200) {
                                return callback(null, channelName, channelId);
                            }
                            return callback('Leave the channel');
                        });
                    },

                ],
                function(err, channelName, channelId) {
                    if(err) {
                        console.log("ERROR: " + err);
                        return res.status(404).json({ error: "Error: " + err});
                    }

                    var signature = crypto.createHmac('sha1', application.slack_api_token).update(channelId).digest('hex');

                    return res.status(200).json({ success: true, channel: channelId, channelName: channelName, signature: signature });
                }
            );
        });

    });

};
<|endoftext|>


<commit_before>angular.module('oath',
  ['ionic',
  'oath.progressCtrl',
  'oath.rootCtrl',
  'oath.paymentCtrl',
  'oath.endConditionCtrls',
  'oath.reportCtrls',
  'oath.goalCtrls',
  'oath.tabCtrl',
  'oath.userFactory',
  'oath.goalFactory',
  'oath.paymentFactory',
  'chart.js',
  'tc.chartjs'
  ])

.run(function($ionicPlatform) {
  $ionicPlatform.ready(function() {
    // Hide the accessory bar by default (remove this to show the accessory bar above the keyboard
    // for form inputs)
    if (window.cordova && window.cordova.plugins.Keyboard) {
      cordova.plugins.Keyboard.hideKeyboardAccessoryBar(true);
    }
    if (window.StatusBar) {
      // org.apache.cordova.statusbar required
      StatusBar.styleDefault();
    }
  });
})

.config(function($stateProvider, $urlRouterProvider) {
  $stateProvider
  .state('root', {
    url: '/',
    cache: false,
    controller: 'RootCtrl',
    templateUrl: 'templates/loading.html'
  })

  .state('login', {
    url: '/login',
    templateUrl: 'templates/login.html'
  })

  .state('localLogin', {
    url: '/locallogin',
    templateUrl: 'templates/localLogin.html'
  })

  .state('localSigup', {
    url: '/localsignup',
    templateUrl: 'templates/localSignup.html'
  })

  .state('goaltype', {
    cache: false,
    url: '/goaltype',
    templateUrl: 'templates/goaltype.html',
    controller: 'GoalCtrl',
  })

  .state('signup', {
    url: '/signup',
    templateUrl: 'templates/signup.html'
  })
  .state('comingsoon', {
    url: '/comingsoon',
    templateUrl: 'templates/comingsoon.html'
  })

  .state('deviceAuth', {
    url: '/deviceauth',
    templateUrl: 'templates/deviceAuth.html',
    controller: 'GoalCtrl'
  })

  .state('goaldetails', {
    cache: false,
    url: '/goaldetails',
    templateUrl: 'templates/goaldetails.html',
    controller: 'GoalDetailCtrl'
  })

  .state('goalsuccess', {
    cache: false,
    url: '/goalsuccess',
    templateUrl: 'templates/goalsuccess.html',
    controller: 'GoalSuccessCtrl'
  })

  .state('goalfailure', {
    cache: false,
    url: '/goalfailure',
    templateUrl: 'templates/goalfailure.html',
    controller: 'GoalFailureCtrl'
  })

  .state('payment', {
    url: '/payment',
    templateUrl: 'templates/payment.html',
    controller: 'PaymentCtrl',
    cache: false
  })

  .state('progress', {
    cache: false,
    url: '/progress',
    templateUrl: 'templates/progress.html',
    controller: 'ProgressCtrl'
  })

  .state('successreport', {
    url: '/successreport',
    templateUrl: 'templates/success.html',
    controller: 'SuccessReportCtrl'
  })

  .state('failurereport', {
    url: '/failurereport',
    templateUrl: 'templates/failure.html',
    controller: 'FailureReportCtrl'
  })

  .state('settings', {
    url: '/settings',
    templateUrl: 'templates/settings.html'
  });

  $urlRouterProvider.otherwise('/');
});
<commit_msg>(feat) Stop autoscroll on focus when keyboard is open<commit_after>angular.module('oath',
  ['ionic',
  'oath.progressCtrl',
  'oath.rootCtrl',
  'oath.paymentCtrl',
  'oath.endConditionCtrls',
  'oath.reportCtrls',
  'oath.goalCtrls',
  'oath.tabCtrl',
  'oath.userFactory',
  'oath.goalFactory',
  'oath.paymentFactory',
  'chart.js',
  'tc.chartjs'
  ])

.run(function($ionicPlatform) {
  $ionicPlatform.ready(function() {
    // Hide the accessory bar by default (remove this to show the accessory bar above the keyboard
    // for form inputs)
    if (window.cordova && window.cordova.plugins.Keyboard) {
      cordova.plugins.Keyboard.hideKeyboardAccessoryBar(true);
      cordova.plugins.Keyboard.disableScroll(true); //we added this to try to stop auto-scroll on payment page. this likely might not do the trick and we might need to remove it
    }
    if (window.StatusBar) {
      // org.apache.cordova.statusbar required
      StatusBar.styleDefault();
    }
  });
})

.config(function($stateProvider, $urlRouterProvider) {
  $stateProvider
  .state('root', {
    url: '/',
    cache: false,
    controller: 'RootCtrl',
    templateUrl: 'templates/loading.html'
  })

  .state('login', {
    url: '/login',
    templateUrl: 'templates/login.html'
  })

  .state('localLogin', {
    url: '/locallogin',
    templateUrl: 'templates/localLogin.html'
  })

  .state('localSigup', {
    url: '/localsignup',
    templateUrl: 'templates/localSignup.html'
  })

  .state('goaltype', {
    cache: false,
    url: '/goaltype',
    templateUrl: 'templates/goaltype.html',
    controller: 'GoalCtrl',
  })

  .state('signup', {
    url: '/signup',
    templateUrl: 'templates/signup.html'
  })
  .state('comingsoon', {
    url: '/comingsoon',
    templateUrl: 'templates/comingsoon.html'
  })

  .state('deviceAuth', {
    url: '/deviceauth',
    templateUrl: 'templates/deviceAuth.html',
    controller: 'GoalCtrl'
  })

  .state('goaldetails', {
    cache: false,
    url: '/goaldetails',
    templateUrl: 'templates/goaldetails.html',
    controller: 'GoalDetailCtrl'
  })

  .state('goalsuccess', {
    cache: false,
    url: '/goalsuccess',
    templateUrl: 'templates/goalsuccess.html',
    controller: 'GoalSuccessCtrl'
  })

  .state('goalfailure', {
    cache: false,
    url: '/goalfailure',
    templateUrl: 'templates/goalfailure.html',
    controller: 'GoalFailureCtrl'
  })

  .state('payment', {
    url: '/payment',
    templateUrl: 'templates/payment.html',
    controller: 'PaymentCtrl',
    cache: false
  })

  .state('progress', {
    cache: false,
    url: '/progress',
    templateUrl: 'templates/progress.html',
    controller: 'ProgressCtrl'
  })

  .state('successreport', {
    url: '/successreport',
    templateUrl: 'templates/success.html',
    controller: 'SuccessReportCtrl'
  })

  .state('failurereport', {
    url: '/failurereport',
    templateUrl: 'templates/failure.html',
    controller: 'FailureReportCtrl'
  })

  .state('settings', {
    url: '/settings',
    templateUrl: 'templates/settings.html'
  });

  $urlRouterProvider.otherwise('/');
});
<|endoftext|>


<commit_before>require('jquery');

/*

AutoComplete module
- Create an autoComplete with the following markup
- Create a JSON object as a global variable, the structure is demonstrated below
- Optionally provide a target input to update with suggestions value

Auto complete html markup:

    <input type="text"
           name="country-code-auto-complete"
           id="country-code-auto-complete"
           class="form-control form-control--block js-choose-country-auto-complete"
           autocomplete="off"
           spellcheck="false"
           required

           // Either pass a global js object to data-suggestions (see schema below):
           data-suggestions="countries"
           // or specify a select input ID to act as source of data:
           data-select-elem="mySelectID"

           // Optionally specify an input ID to be sent the selected suggestion when picked
           // This can be the select input specified in data-select-elem
           data-target-elem="myOutputID"

           data-rule-suggestion="true"
           aria-autocomplete="list"
           aria-haspopup="country-code-suggestions"
           aria-activedescendant />

Data format:

*/

var suggestions;
var $suggestionsContainer;
var $autoCompleteInputElem;
var $targetInput;
var suggestionDisplayFormat;
var $suggestionsStatusMessage;
var $clearInputButton;

/**
 * Display suggestions
 * @param $suggestionsContainer
 * @param matches
 * @param match
 */
var displaySuggestions = function ($suggestionsContainer, matches, match) {
  var ulHtmlFragment = document.createElement('ul');
  var liHtmlFragment = document.createElement('li');
  var containerFragment = document.createElement('div');

  var createSuggestion = function (index, suggestion) {
    var positionalClassNames = [];
    var li = liHtmlFragment.cloneNode();
    var pattern = new RegExp('^(' + match + ')', 'i');
    var html;

    suggestion.title.replace(pattern, '$1');
    html = (typeof suggestionDisplayFormat === 'function') ? suggestionDisplayFormat(suggestion.title, suggestion.value) : suggestion.title;

    if (index === 0) {
      positionalClassNames.push('suggestion--first');
    }
    if (index === matches.length - 1) {
      positionalClassNames.push('suggestion--last');
    }

    li.innerHTML = html;
    li.className = 'suggestion ' + positionalClassNames.join(' ');
    li.setAttribute('data-suggestion-value', suggestion.value);
    li.setAttribute('data-suggestion-title', suggestion.title);
    li.setAttribute('role', 'option');
    ulHtmlFragment.className = 'suggestions-list';
    ulHtmlFragment.setAttribute('role', 'listbox');
    ulHtmlFragment.appendChild(li);
  };

  $(matches).each(function (index, suggestion) {
    createSuggestion(index, suggestion);
  });

  $suggestionsStatusMessage.text(matches.length + ' suggestion' + (matches.length > 1 ? 's' : '') + ' available, please navigate by using up and down');
  $autoCompleteInputElem.addClass('has-suggestions');

  containerFragment.appendChild(ulHtmlFragment);
  $suggestionsContainer.html(containerFragment.innerHTML);
};

/**
 * close suggestions display
 */
var closeSuggestions = function () {
  $suggestionsContainer.html('');
  $autoCompleteInputElem.removeClass('has-suggestions');
};

/**
 * close suggestions display and focus the auto complete element and place cursor at end of text in input
 */
var closeSuggestionsAndFocus = function () {
  closeSuggestions();
  $autoCompleteInputElem.focus().val($autoCompleteInputElem.val()); //place cursor at end of input text
};

/**
 * empty input, focus input, close suggestions display and empty target input
 */
var clearInput = function () {
  $autoCompleteInputElem.val('').focus();
  updateAutoCompleteAttribData('suggestionValue', '');
  closeSuggestions();
  updateTargetInput(0);
};

/**
 * place cursor at end of input and updateTarget element
 * @param title
 * @param value
 */
var updateInput = function (title, value) {
  $autoCompleteInputElem.val(title).val($autoCompleteInputElem.val()); //place cursor at end of input text
  updateAutoCompleteAttribData('suggestionValue', value.toString());
  updateTargetInput(value);
};

/**
 * Allow auto complete to control and pass over the .value of your suggestion to another optional input
 * specified on creation of the autoComplete
 * @param value
 */
var updateTargetInput = function (value) {
  if ($targetInput) {
    $targetInput.val(value);
  }
};

/**
  update a data attribute on the autoCompleteInputElem
  @param dataEl
  @param value
  */
var updateAutoCompleteAttribData = function (dataEl, value) {
  $autoCompleteInputElem.data(dataEl, value);
};

/**
 * Controls for the following keys to navigate the suggestions list
 * 40 (down) - move down the suggestions list
 * 38 (up) - move up teh suggestions list
 * 27 (esc) - close from the suggestion list
 * 13 (enter) - choose item if list is open
 * @param keyCode
 * @param $suggestionsContainer
 */
var suggestionsKeyControl = function (keyCode, $suggestionsContainer) {

  //40 (down), 38 (up)
  if ((keyCode === 38 || keyCode === 40) && $suggestionsContainer.html()) {

    var $next;
    var $selected = $suggestionsContainer.find('.suggestion--selected').first();
    var updateAutoCompleteInput = function ($elem) {
      updateInput($elem.data('suggestion-title'), $elem.data('suggestion-value'));
    };

    if (!$selected.length) {
      if (keyCode === 40) {
        $next = $suggestionsContainer.find('li').first().addClass('suggestion--selected').attr('aria-selected', true);
        updateAutoCompleteInput($next);
      }
    } else {
      $next = (keyCode === 40) ? $selected.next() : $selected.prev();
      if ($next.length) {
        $selected.removeClass('suggestion--selected').removeAttr('aria-selected');
        $next.addClass('suggestion--selected').attr('aria-selected', true);
        updateAutoCompleteInput($next);
      } else {
        if ($selected.prev().length === 0 && keyCode === 38) { //hide when reaching top of suggestions
          $selected.removeClass('suggestion--selected').removeAttr('aria-selected');
          closeSuggestionsAndFocus();
        }
      }
    }
  } else if (keyCode === 27 || keyCode === 13) { //27 (esc), 13 (enter)
    closeSuggestionsAndFocus();
  }
};

var addEventListeners = function () {
  inputKeyDownEvent();
  inputKeyupEvent();
  inputBlurEvent();
  clearSuggestionsEvent();
  suggestionsEvent();
};

/**
 * keydown event for auto complete input
 * keydown used to capture enter key (13) before form is submitted if we have suggestions
 * allow suggestion list key controls through
 * 40 (down)
 * 38 (up)
 * 27 (esc)
 * 13 (enter)
 */
var inputKeyDownEvent = function () {
  $autoCompleteInputElem.on('keydown', function (event) {
    var keyCode = event.which;

    if (keyCode === 13 && $suggestionsContainer.html()) {
      event.preventDefault();
    }

    if (keyCode === 38 || keyCode === 40 || keyCode === 13 || keyCode === 27) {
      suggestionsKeyControl(keyCode, $suggestionsContainer);
    }
  });
};

/**
 * keyup event dependant on auto complete input, if key is not a suggestion list key control and we have a value in
 * the auto complete input the search through suggestions.
 *
 * if we have suggestions display them
 * If we have no input value them clear suggestions
 */
var inputKeyupEvent = function () {
  $autoCompleteInputElem.on('keyup', function (event) {
    var inputVal = $(event.target).val();
    var matches = [];
    var keyCode = event.which;

    if(inputVal.length < 2) {
      updateAutoCompleteAttribData('suggestionValue', '');
    }

    if ((keyCode !== 36 && keyCode !== 38 && keyCode !== 40 && keyCode !== 13 && keyCode !== 27) && inputVal) {

      $clearInputButton.removeClass('hidden');

      $(suggestions).each(function (index, suggestion) {
        var regEx;

        try {
          regEx = new RegExp('^(' + inputVal + ')', 'i');
          if (regEx.test(suggestion.title)) {
            matches.push(suggestion);
          }
        } catch (e) {
          //TODO add reporting?
        }

      });

      if (matches.length) {
        displaySuggestions($suggestionsContainer, matches, inputVal);
      }
    } else if (!inputVal) {
      $clearInputButton.addClass('hidden');
      closeSuggestionsAndFocus();
      $suggestionsStatusMessage.text('');
    }
  });
};

/**
 * on blur check to see if what has been entered matches a suggestion, in case the customer hasn't interacted with
 * the suggestions list
 */
var inputBlurEvent = function () {
  $autoCompleteInputElem.on('blur', function (event) {
    isMatchingSuggestion(event.target.value);
    closeSuggestions();
  });
};

/**
 * check to see if the value specified in teh auto complete input matches a unique title found in suggestions
 * if it does then update it and the target input
 * @param value
 */
var isMatchingSuggestion = function (value) {
  var matchFound = false;

  if (value) {
    $(suggestions).each(function (index, suggestion) {
    var returnValue = true;

      if (value.toLowerCase() === suggestion.title.toLowerCase()) {
        if ($autoCompleteInputElem.data('suggestionValue') === suggestion.value) {
          matchFound = true;
          returnValue = false;
        } else if ($autoCompleteInputElem.data('suggestionValue') === '') {
          updateInput(suggestion.title, suggestion.value);
          matchFound = true;
          returnValue = false;
        }

        return returnValue;
      }
    });

    if (!matchFound) { //clear out the input
      updateTargetInput(0);
      updateAutoCompleteAttribData('suggestionValue', '');
    }
  }
};

/**
 * (x) button on input to clear out the input value, close suggestions and clear out input target
 */
var clearSuggestionsEvent = function () {
  $clearInputButton.on('mousedown', function (event) {
    event.preventDefault();
    clearInput();
    updateAutoCompleteAttribData('suggestionValue','');
    $clearInputButton.addClass('hidden');
  });
};

/**
 * update input to suggested values when mouse click actioned on suggestion list
 * mousedown event used so this fires before blur event
 */
var suggestionsEvent = function () {
  $suggestionsContainer.on('mousedown', '.suggestion', function (event) {
    var $suggestion = $(event.target);
    event.preventDefault();

    updateInput($suggestion.data('suggestion-title'), $suggestion.data('suggestion-value'));
    closeSuggestions();
  });
};

/**
  * Either grab a given global variable or generate one from a given select input
*/
var getSuggestions = function (el) {
  var suggestions;
  var dataLinked = $('select[id="' + el.attr("data-select-elem") + '"]');
  if(dataLinked.length > 0){
    // using a select input in-page as the source for the suggestions
    // this is useful when using a select as a fallback for the autocomplete
    suggestions = [];
    dataLinked.find('option').each(function(){
      if($(this).text() > ""){
        suggestions.push({"title":$(this).text(), "value":$(this).attr("value")});
      }
    });
    // need to populate the data-suggestions attribute from this for validation
    // generate a global variable to hold the data and insert into data-suggestions attribute
    window.hmrcAutocompleteData = suggestions;
    el.attr("data-suggestions", "hmrcAutocompleteData");
  } else {
    // allow custom variable
    // this works in the same way as the legacy version by passing in an existing global object name
    suggestions = window[el.attr("data-suggestions")];
  }
  return suggestions;
};

/**
  * Find input to post value back to
*/
var getTargetInput = function (el) {
  var target;
  // input to post back a selection from the autocomplete
  var dataPostTo = $('[id="' + el.attr("data-target-elem") + '"]');
  if(dataPostTo.length > 0){
    target = dataPostTo;
  }
  return target;
};

/**
 * setup variables and create the autoComplete
 *
 * @param $autoCompleteInputElem
 *
 * @param suggestionFormat - [optional]
 * Format for the suggestion to be displayed in the suggestion list, this will default to suggestion.title if a format is not supplied
 *
 */
var setup = function ($elem, suggestionsData, $targetInputElem, suggestionFormat) {
  /*
  * Note:
  * $targetInputElem and suggestionsData are for legacy support only
  * please use data attributes (data-suggestions and data-target-elem) to pass their values
  */

  if ($elem.length) {
    $autoCompleteInputElem = $elem;

    /*
    * This is set via the data-target-elem attribute on $autoCompleteInputElem
    * Optional input/select element to apply the suggestion.value to when a suggestion is selected.
    * If this is not supplied then the autoComplete input will contain the
    * suggestion.title which will be sent to the backend as the value of the $autoCompleteInputElem
    */
    $targetInput = $targetInputElem || getTargetInput($elem);

    /*
    * This is set via the data-suggestions attribute on $autoCompleteInputElem
    * e.g. data-suggestions="countries"
    * This holds the suggestions in a global js object (in the above case 'countries')
    * Format is [{title: "United Kingdom", value: "UK"}, {title: "United States", value: "USA"}]
    * This is also used for validation
    * If the id of a select input is provided instead via the data-select-elem attribute then the data-suggestions
    * attribute will be auto-generated (should not be supplied as will be overridden) and a suggestions object generated from its options
    */
    suggestions = suggestionsData || getSuggestions($elem);

    suggestionDisplayFormat = suggestionFormat;
    $clearInputButton = $('.js-suggestions-clear');
    $suggestionsContainer = $('.js-suggestions').first();
    $suggestionsStatusMessage = $('.js-suggestions-status-message').first();

    addEventListeners();
  }
};

module.exports = setup;
<commit_msg>Allow setting of existing value to autocomplete select input (#728)<commit_after>require('jquery');

/*

AutoComplete module
- Create an autoComplete with the following markup
- Create a JSON object as a global variable, the structure is demonstrated below
- Optionally provide a target input to update with suggestions value

Auto complete html markup:

    <input type="text"
           name="country-code-auto-complete"
           id="country-code-auto-complete"
           class="form-control form-control--block js-choose-country-auto-complete"
           autocomplete="off"
           spellcheck="false"
           required

           // Either pass a global js object to data-suggestions (see schema below):
           data-suggestions="countries"
           // or specify a select input ID to act as source of data:
           data-select-elem="mySelectID"

           // Optionally specify an input ID to be sent the selected suggestion when picked
           // This can be the select input specified in data-select-elem
           data-target-elem="myOutputID"

           data-rule-suggestion="true"
           aria-autocomplete="list"
           aria-haspopup="country-code-suggestions"
           aria-activedescendant />

Data format:

*/

var suggestions;
var $suggestionsContainer;
var $autoCompleteInputElem;
var $targetInput;
var suggestionDisplayFormat;
var $suggestionsStatusMessage;
var $clearInputButton;

/**
 * Display suggestions
 * @param $suggestionsContainer
 * @param matches
 * @param match
 */
var displaySuggestions = function ($suggestionsContainer, matches, match) {
  var ulHtmlFragment = document.createElement('ul');
  var liHtmlFragment = document.createElement('li');
  var containerFragment = document.createElement('div');

  var createSuggestion = function (index, suggestion) {
    var positionalClassNames = [];
    var li = liHtmlFragment.cloneNode();
    var pattern = new RegExp('^(' + match + ')', 'i');
    var html;

    suggestion.title.replace(pattern, '$1');
    html = (typeof suggestionDisplayFormat === 'function') ? suggestionDisplayFormat(suggestion.title, suggestion.value) : suggestion.title;

    if (index === 0) {
      positionalClassNames.push('suggestion--first');
    }
    if (index === matches.length - 1) {
      positionalClassNames.push('suggestion--last');
    }

    li.innerHTML = html;
    li.className = 'suggestion ' + positionalClassNames.join(' ');
    li.setAttribute('data-suggestion-value', suggestion.value);
    li.setAttribute('data-suggestion-title', suggestion.title);
    li.setAttribute('role', 'option');
    ulHtmlFragment.className = 'suggestions-list';
    ulHtmlFragment.setAttribute('role', 'listbox');
    ulHtmlFragment.appendChild(li);
  };

  $(matches).each(function (index, suggestion) {
    createSuggestion(index, suggestion);
  });

  $suggestionsStatusMessage.text(matches.length + ' suggestion' + (matches.length > 1 ? 's' : '') + ' available, please navigate by using up and down');
  $autoCompleteInputElem.addClass('has-suggestions');

  containerFragment.appendChild(ulHtmlFragment);
  $suggestionsContainer.html(containerFragment.innerHTML);
};

/**
 * close suggestions display
 */
var closeSuggestions = function () {
  $suggestionsContainer.html('');
  $autoCompleteInputElem.removeClass('has-suggestions');
};

/**
 * close suggestions display and focus the auto complete element and place cursor at end of text in input
 */
var closeSuggestionsAndFocus = function () {
  closeSuggestions();
  $autoCompleteInputElem.focus().val($autoCompleteInputElem.val()); //place cursor at end of input text
};

/**
 * empty input, focus input, close suggestions display and empty target input
 */
var clearInput = function () {
  $autoCompleteInputElem.val('').focus();
  updateAutoCompleteAttribData('suggestionValue', '');
  closeSuggestions();
  updateTargetInput(0);
};

/**
 * place cursor at end of input and updateTarget element
 * @param title
 * @param value
 */
var updateInput = function (title, value) {
  $autoCompleteInputElem.val(title).val($autoCompleteInputElem.val()); //place cursor at end of input text
  updateAutoCompleteAttribData('suggestionValue', value.toString());
  updateTargetInput(value);
};

/**
 * Allow auto complete to control and pass over the .value of your suggestion to another optional input
 * specified on creation of the autoComplete
 * @param value
 */
var updateTargetInput = function (value) {
  if ($targetInput) {
    $targetInput.val(value);
  }
};

/**
  update a data attribute on the autoCompleteInputElem
  @param dataEl
  @param value
  */
var updateAutoCompleteAttribData = function (dataEl, value) {
  $autoCompleteInputElem.data(dataEl, value);
};

/**
 * Controls for the following keys to navigate the suggestions list
 * 40 (down) - move down the suggestions list
 * 38 (up) - move up teh suggestions list
 * 27 (esc) - close from the suggestion list
 * 13 (enter) - choose item if list is open
 * @param keyCode
 * @param $suggestionsContainer
 */
var suggestionsKeyControl = function (keyCode, $suggestionsContainer) {

  //40 (down), 38 (up)
  if ((keyCode === 38 || keyCode === 40) && $suggestionsContainer.html()) {

    var $next;
    var $selected = $suggestionsContainer.find('.suggestion--selected').first();
    var updateAutoCompleteInput = function ($elem) {
      updateInput($elem.data('suggestion-title'), $elem.data('suggestion-value'));
    };

    if (!$selected.length) {
      if (keyCode === 40) {
        $next = $suggestionsContainer.find('li').first().addClass('suggestion--selected').attr('aria-selected', true);
        updateAutoCompleteInput($next);
      }
    } else {
      $next = (keyCode === 40) ? $selected.next() : $selected.prev();
      if ($next.length) {
        $selected.removeClass('suggestion--selected').removeAttr('aria-selected');
        $next.addClass('suggestion--selected').attr('aria-selected', true);
        updateAutoCompleteInput($next);
      } else {
        if ($selected.prev().length === 0 && keyCode === 38) { //hide when reaching top of suggestions
          $selected.removeClass('suggestion--selected').removeAttr('aria-selected');
          closeSuggestionsAndFocus();
        }
      }
    }
  } else if (keyCode === 27 || keyCode === 13) { //27 (esc), 13 (enter)
    closeSuggestionsAndFocus();
  }
};

var addEventListeners = function () {
  inputKeyDownEvent();
  inputKeyupEvent();
  inputBlurEvent();
  clearSuggestionsEvent();
  suggestionsEvent();
};

/**
 * keydown event for auto complete input
 * keydown used to capture enter key (13) before form is submitted if we have suggestions
 * allow suggestion list key controls through
 * 40 (down)
 * 38 (up)
 * 27 (esc)
 * 13 (enter)
 */
var inputKeyDownEvent = function () {
  $autoCompleteInputElem.on('keydown', function (event) {
    var keyCode = event.which;

    if (keyCode === 13 && $suggestionsContainer.html()) {
      event.preventDefault();
    }

    if (keyCode === 38 || keyCode === 40 || keyCode === 13 || keyCode === 27) {
      suggestionsKeyControl(keyCode, $suggestionsContainer);
    }
  });
};

/**
 * keyup event dependant on auto complete input, if key is not a suggestion list key control and we have a value in
 * the auto complete input the search through suggestions.
 *
 * if we have suggestions display them
 * If we have no input value them clear suggestions
 */
var inputKeyupEvent = function () {
  $autoCompleteInputElem.on('keyup', function (event) {
    var inputVal = $(event.target).val();
    var matches = [];
    var keyCode = event.which;

    if(inputVal.length < 2) {
      updateAutoCompleteAttribData('suggestionValue', '');
    }

    if ((keyCode !== 36 && keyCode !== 38 && keyCode !== 40 && keyCode !== 13 && keyCode !== 27) && inputVal) {

      $clearInputButton.removeClass('hidden');

      $(suggestions).each(function (index, suggestion) {
        var regEx;

        try {
          regEx = new RegExp('^(' + inputVal + ')', 'i');
          if (regEx.test(suggestion.title)) {
            matches.push(suggestion);
          }
        } catch (e) {
          //TODO add reporting?
        }

      });

      if (matches.length) {
        displaySuggestions($suggestionsContainer, matches, inputVal);
      }
    } else if (!inputVal) {
      $clearInputButton.addClass('hidden');
      closeSuggestionsAndFocus();
      $suggestionsStatusMessage.text('');
    }
  });
};

/**
 * on blur check to see if what has been entered matches a suggestion, in case the customer hasn't interacted with
 * the suggestions list
 */
var inputBlurEvent = function () {
  $autoCompleteInputElem.on('blur', function (event) {
    isMatchingSuggestion(event.target.value);
    closeSuggestions();
  });
};

/**
 * check to see if the value specified in teh auto complete input matches a unique title found in suggestions
 * if it does then update it and the target input
 * @param value
 */
var isMatchingSuggestion = function (value) {
  var matchFound = false;

  if (value) {
    $(suggestions).each(function (index, suggestion) {
    var returnValue = true;

      if (value.toLowerCase() === suggestion.title.toLowerCase()) {
        if ($autoCompleteInputElem.data('suggestionValue') === suggestion.value) {
          matchFound = true;
          returnValue = false;
        } else if ($autoCompleteInputElem.data('suggestionValue') === '') {
          updateInput(suggestion.title, suggestion.value);
          matchFound = true;
          returnValue = false;
        }

        return returnValue;
      }
    });

    if (!matchFound) { //clear out the input
      updateTargetInput(0);
      updateAutoCompleteAttribData('suggestionValue', '');
    }
  }
};

/**
 * (x) button on input to clear out the input value, close suggestions and clear out input target
 */
var clearSuggestionsEvent = function () {
  $clearInputButton.on('mousedown', function (event) {
    event.preventDefault();
    clearInput();
    updateAutoCompleteAttribData('suggestionValue','');
    $clearInputButton.addClass('hidden');
  });
};

/**
 * update input to suggested values when mouse click actioned on suggestion list
 * mousedown event used so this fires before blur event
 */
var suggestionsEvent = function () {
  $suggestionsContainer.on('mousedown', '.suggestion', function (event) {
    var $suggestion = $(event.target);
    event.preventDefault();

    updateInput($suggestion.data('suggestion-title'), $suggestion.data('suggestion-value'));
    closeSuggestions();
  });
};

/**
  * Get a linked select input
**/
var getDataLinked = function(el){
  var dataLinked = $('select[id="' + el.attr("data-select-elem") + '"]');
  return dataLinked;
};

/**
  * If a select elem is being used, load value from there
**/
var loadExistingValue = function (el) {
    var dataLinked = getDataLinked(el);
    var selectedOption = dataLinked.find('option:selected')[0];
    if(selectedOption.value !== ""){
        el.val(selectedOption.text);
    }
};

/**
  * Either grab a given global variable or generate one from a given select input
*/
var getSuggestions = function (el) {
  var suggestions;
  var dataLinked = getDataLinked(el);
  if(dataLinked.length > 0){
    // using a select input in-page as the source for the suggestions
    // this is useful when using a select as a fallback for the autocomplete
    suggestions = [];
    dataLinked.find('option').each(function(){
      if($(this).text() > ""){
        suggestions.push({"title":$(this).text(), "value":$(this).attr("value")});
      }
    });
    // need to populate the data-suggestions attribute from this for validation
    // generate a global variable to hold the data and insert into data-suggestions attribute
    window.hmrcAutocompleteData = suggestions;
    el.attr("data-suggestions", "hmrcAutocompleteData");
    // update text input with a pre-selected select option
    loadExistingValue(el);
  } else {
    // allow custom variable
    // this works in the same way as the legacy version by passing in an existing global object name
    suggestions = window[el.attr("data-suggestions")];
  }
  return suggestions;
};

/**
  * Find input to post value back to
*/
var getTargetInput = function (el) {
  var target;
  // input to post back a selection from the autocomplete
  var dataPostTo = $('[id="' + el.attr("data-target-elem") + '"]');
  if(dataPostTo.length > 0){
    target = dataPostTo;
  }
  return target;
};

/**
 * setup variables and create the autoComplete
 *
 * @param $autoCompleteInputElem
 *
 * @param suggestionFormat - [optional]
 * Format for the suggestion to be displayed in the suggestion list, this will default to suggestion.title if a format is not supplied
 *
 */
var setup = function ($elem, suggestionsData, $targetInputElem, suggestionFormat) {
  /*
  * Note:
  * $targetInputElem and suggestionsData are for legacy support only
  * please use data attributes (data-suggestions and data-target-elem) to pass their values
  */

  if ($elem.length) {
    $autoCompleteInputElem = $elem;

    /*
    * This is set via the data-target-elem attribute on $autoCompleteInputElem
    * Optional input/select element to apply the suggestion.value to when a suggestion is selected.
    * If this is not supplied then the autoComplete input will contain the
    * suggestion.title which will be sent to the backend as the value of the $autoCompleteInputElem
    */
    $targetInput = $targetInputElem || getTargetInput($elem);

    /*
    * This is set via the data-suggestions attribute on $autoCompleteInputElem
    * e.g. data-suggestions="countries"
    * This holds the suggestions in a global js object (in the above case 'countries')
    * Format is [{title: "United Kingdom", value: "UK"}, {title: "United States", value: "USA"}]
    * This is also used for validation
    * If the id of a select input is provided instead via the data-select-elem attribute then the data-suggestions
    * attribute will be auto-generated (should not be supplied as will be overridden) and a suggestions object generated from its options
    */
    suggestions = suggestionsData || getSuggestions($elem);

    suggestionDisplayFormat = suggestionFormat;
    $clearInputButton = $('.js-suggestions-clear');
    $suggestionsContainer = $('.js-suggestions').first();
    $suggestionsStatusMessage = $('.js-suggestions-status-message').first();

    addEventListeners();
  }
};

module.exports = setup;
<|endoftext|>