HEX
Server: nginx/1.24.0
System: Linux nowruzgan 6.8.0-57-generic #59-Ubuntu SMP PREEMPT_DYNAMIC Sat Mar 15 17:40:59 UTC 2025 x86_64
User: babak (1000)
PHP: 8.3.6
Disabled: NONE
Upload Files
File: //var/dev/farhangmoaser/web/routes/api/1.0/dictionary.js
/**
 * Express endpoints for /dictionary address
 * Version: 0.1
 * Author: Babak Vandad
 *
 * Restful api for addresses:
 * 		GET			/dictionary/cat
 * 		PUT			/dictionary/cat
 * 		POST		/dictionary/cat
 * 		DELETE		/dictionary/cat/:id
 * 		GET			/dictionary/
 * 		GET			/dictionary/:uuid
 * 		PUT			/dictionary/
 * 		POST		/dictionary/:uuid
 * 		DELETE		/dictionary/:uuid
 */

var express = require('express');
var router = express.Router();
var fs = require('fs');
var glob = require('glob');
var path = require('path');
var consts = require(path.join(BASEDIR, 'consts'));
var authHelper = require(path.join(BASEDIR, 'helpers/auth'));
var UserAccessModel = require(path.join(BASEDIR, 'models/userAccess'));
var LongTextModel = require(path.join(BASEDIR, 'models/longText'));
var DictionaryCatModel = require(path.join(BASEDIR, 'models/dictionaryCat'));
var DictionaryModel = require(path.join(BASEDIR, 'models/dictionary'));
var db = require(path.join(BASEDIR, 'connectors/mysql'));

var authenticate = authHelper.authenticate;
var access = authHelper.access;

/**
 * Fail a request by releasing database connection, rollback transaction
 * and sending the proper error code and message with 4xx or 5xx status codes.
 * @param  {object}		express response object
 * @param  {object}		db connection
 * @param  {object}		error object from costs.js
 * @param  {integer}	override status code (if you want to send 200 instead of 4xx/5xx)
 * @return {function}	the function to fail the request.
 */
var fail = function(response, conn, error, status){
	return function(){
		if(conn && conn.release)
			conn.release();
		response.status(status ? status : error.status).json({error: error.code, message: error.message});
	};
};

/**
 * Sends data to the client, commits transaction and releases the db connection
 * @param  {object}		express response object
 * @param  {object}		db connection
 * @param  {object}		data for the client
 */
var send = function(conn, res, data){
	conn.release();
	if(data)
		res.json(data);
	else
		res.end();
};

/* Fetches list of dictionary categories. */
router.get('/cat/', authenticate, access('dictionary:cat:get', 'نمایش دسته‌بندی فرهنگ‌ها'), function(req, res, next) {
	db.getConnection(function(err, conn){
		if(err) return fail(res, conn, err)();

		DictionaryCatModel.list(conn).then(
			function (records) {
				send(conn, res, records);
			}, fail(res, conn, consts.e.ERR_DB_ERROR)
		);
	});
});

/**
 *  Adds a new dictionary category
 *  Category title is required
 */
router.put('/cat/', authenticate, access('dictionary:cat:put', 'افزودن دسته جدید'), function(req, res, next) {
	if(!req.body.title)
		return fail(res, null, consts.e.ERR_REQUIRED_FIELDS)();

	db.getConnection(function(err, conn){
		if(err) return fail(res, conn, err)();

		var dictionaryCatModel = new DictionaryCatModel(conn, {title: req.body.title});
		dictionaryCatModel.load().then(
			function(record){
				if(record) return fail(res, conn, consts.e.ERR_MALFORMED_REQUEST)();
				dictionaryCatModel.save().then(
					function(record) {
						send(conn, res, record.fields());
					}, fail(res, conn, consts.e.ERR_DB_ERROR));
			}, fail(res, conn, consts.e.ERR_DB_ERROR));
	});
});

/**
 * Updates an existing dictionary category.
 * Category id or title is required.
 */
router.post('/cat/', authenticate, access('dictionary:cat:post', 'ویرایش نام دسته'), function(req, res, next) {
	if(!req.body.title || !req.body.id)
		return fail(res, null, consts.e.ERR_REQUIRED_FIELDS)();

	db.getConnection(function(err, conn){
		if(err) return fail(res, conn, err)();

		var dictionaryCatModel = new DictionaryCatModel(conn, {id: req.body.id, title: req.body.title});
		dictionaryCatModel.save().then(
			function(record) {
				send(conn, res, record.fields());
			}, fail(res, conn, consts.e.ERR_DB_ERROR)
		);
	});
});

/**
 * Deletes a dictionary category.
 * Category id is required.
 */
router.delete('/cat/:id', authenticate, access('dictionary:cat:delete-id', 'حذف دسته'), function(req, res, next) {
	if(!req.params.id)
		return fail(res, null, consts.e.ERR_REQUIRED_FIELDS)();

	db.getConnection(function(err, conn){
		if(err) return fail(res, conn, err)();

		var dictionaryCatModel = new DictionaryCatModel(conn, {id: req.params.id});
		dictionaryCatModel.remove().then(
			function(record) {
				send(conn, res, record.fields());
			}, fail(res, conn, consts.e.ERR_DB_ERROR)
		);
	});
});

/* Fetches list of books. with minimum required info. */
router.get('/', authenticate, access('dictionary:get', 'نمایش فهرست فرهنگ‌ها'), function(req, res, next) {
	db.getConnection(function(err, conn){
		if(err) return fail(res, conn, err)();

		DictionaryModel.list(conn).then(
			function(records){
				send(conn, res, records);//TODO: make it a standard response: {sum: records.length, list: records}
			}, fail(res, conn, consts.e.ERR_DB_ERROR)
		);
	});
});

/* Fetches list of books. with minimum required info. */
router.get('/active', authenticate, access('dictionary:get-active', 'نمایش فهرست فرهنگ‌های فعال'), (req, res, next) => {
	db.getConnection((err, conn) => {
		if(err) return fail(res, conn, err)();

		DictionaryModel.listActives(conn).then(
			records => {
				send(conn, res, {sum: records.length, list: records});
			}, fail(res, conn, consts.e.ERR_DB_ERROR)
		);
	});
});

/**
 * Fetches complete record of a book.
 * A uuid with format of .{8}(-.{4}){3}-.{12} is required.
 */
router.get('/:uuid', authenticate, access('dictionary:get-uuid', 'نمایش جزئیات فرهنگ'), function(req, res, next) {
	if(!/.{8}(-.{4}){3}-.{12}/.exec(req.params.uuid))
		return fail(res, null, consts.e.ERR_MALFORMED_REQUEST)();

	db.getConnection(function(err, conn){
		if(err) return fail(res, conn, err)();

		/* Loads description and related categories of the book. */
		var loadDetails = function(record){
			record.getLangs().then(
				function(langs) {
					record.field('langs', langs.map(function(row){
						return {from: row.lang_from, to: row.lang_to};
					}));
					record.getRelatedCategories().then(
						function(categories) {
							record.field('id', undefined);
							record.field('categories', categories);
							if(record.field('desc')) {
								var longTextModel = new LongTextModel(conn, {id: record.field('desc')});
								longTextModel.load().then(
									function(longtext) {
										record.field('desc', longtext.field('content'));
										send(conn, res, record.fields());
									}, fail(res, conn, consts.e.ERR_MISSING_RECORD));
							}else
								send(conn, res, record.fields());
						}, fail(res, conn, consts.e.ERR_MISSING_RECORD));
				}, fail(res, conn, consts.e.ERR_DB_ERROR));
		};

		var dictionaryModel = new DictionaryModel(conn, {uuid: req.params.uuid});
		dictionaryModel.load().then(
			function(record) {
				if(!record)
					return fail(res, conn, consts.e.ERR_MISSING_RECORD)();
				else{
					if(req.query.recountEntries)
						record.updateEntryCount().then(
							function(){
								loadDetails(record);
							}, fail(res, conn, consts.e.ERR_DB_ERROR));
					else
						loadDetails(record);
				}
			}, fail(res, conn, consts.e.ERR_MISSING_RECORD)
		);
	});
});

/**
 * Adds a new book.
 * At least title, author and pubYear fields must be set.
 */
router.put('/', authenticate, access('dictionary:put', 'افزودن فرهنگ'), function(req, res, next) {
	if(!req.body.title || !req.body.author || !req.body.pubYear)
		return fail(res, null, consts.e.ERR_REQUIRED_FIELDS)();

	db.getConnection(function(err, conn){
		if(err) return fail(res, conn, err)();

		var saveBook = function(book) {
			/* save the book record (source table only) */
			book.save().then(
				function(record) {
					/* update source_lang table */
					record.addLangs(req.body.langs).then(
						function(langs){
							/* update dictionary_cat_rel table */
							record.relateCategories(req.body.categories).then(
								function(categories) {
									if(req.body.cover) {
										/* move the cover image to the cover folder. */
										fs.rename(path.join(BASEDIR, 'private/temp/'+req.body.cover), path.join(BASEDIR, 'private/cover/'+req.body.cover), function(err){
											if(err)
												return fail(res, conn, consts.e.ERR_FILE_ERROR)();

											record.field('desc', req.body.desc);
											record.field('id', undefined);
											send(conn, res, record.fields());
										});
									}else
										send(conn, res, record.fields());
								}, fail(res, conn, consts.e.ERR_DB_ERROR, 200));
						}, fail(res, conn, consts.e.ERR_DB_ERROR, 200));
				}, fail(res, conn, consts.e.ERR_DB_ERROR));
		}

		/* First save the description */
		var dictionaryModel = new DictionaryModel(conn, req.body);
		if(req.body.desc) {
			var longTextModel = new LongTextModel(conn, {content: req.body.desc});
			longTextModel.save().then(
				function(record){
					dictionaryModel.field('desc', record.field('id'));
					saveBook(dictionaryModel);
				}, fail(res, conn, consts.e.ERR_DB_ERROR)
			);
		}else
			saveBook(dictionaryModel);
	});
});

/**
 * Updates an existing book.
 * A uuid with format of .{8}(-.{4}){3}-.{12} is required.
 */
router.post('/:uuid', authenticate, access('dictionary:post-uuid', 'ویرایش فرهنگ'), function(req, res, next) {
	if(!/.{8}(-.{4}){3}-.{12}/.exec(req.params.uuid))
		return fail(res, null, consts.e.ERR_MALFORMED_REQUEST)();

	if(!req.body.title || !req.body.author || !req.body.pubYear)
		return fail(res, null, consts.e.ERR_REQUIRED_FIELDS)();

	db.getConnection(function(err, conn){
		if(err) return fail(res, conn, err)();

		/* load the book */
		var dictionaryModel = new DictionaryModel(conn, {uuid: req.params.uuid});
		dictionaryModel.load().then(
			function(record) {
				if(!record)
					return fail(res, conn, consts.e.ERR_MISSING_RECORD)();

				/* load langs of the book */
				record.getLangs().then(
					function(langs) {
						/* load categories of the book */
						record.getRelatedCategories().then(
							function(categories) {
								var longTextModel = new LongTextModel(conn, {id: record.field('desc')});
								/* load the book description */
								longTextModel.load().then(
									function(description) {
										/* update cover if needed */
										var handleCover = function(callback, _fail){
											if(req.body.cover == record.field('cover'))
												return callback(req.body.cover);
											glob(path.join(BASEDIR, 'private/cover/'+record.field('cover')+'*'), {}, function(err, files){
												if(record.field('cover')) // do not delete every thing!
													files.forEach(function(file){
														fs.unlinkSync(file);
													});

												fs.rename(path.join(BASEDIR, 'private/temp/'+req.body.cover), path.join(BASEDIR, 'private/cover/'+req.body.cover), function(err){
													if(err) return _fail();
													callback(req.body.cover);
												});
											});
										};

										/* update categories if needed */
										var handleCategories = function(callback, _fail){
											oldcats = [];
											categories.forEach(function(cat){
												oldcats.push(cat.id);
											});

											if(req.body.categories.sort().join('-') == oldcats.sort().join('-'))
												return callback();

											record.relateCategories(req.body.categories).then(callback, _fail);
										};

										/* update languges if needed */
										var handleLangs = function(callback, _fail){
											/* make comparable strings */
											var oldlangs = langs.map(function(row){
												return row.from+'->'+row.to;
											}).sort().join('|');
											var newlangs = req.body.langs.map(function(row){
												return row.from+'->'+row.to;
											}).sort().join('|');

											if(newlangs == oldlangs) return callback();
											record.addLangs(req.body.langs).then(callback, _fail);
										};

										/* update book description if needed */
										var handleDescription = function(callback, _fail){
											if(longTextModel.field('content') == req.body.desc)
												return callback();

											longTextModel.field('content', req.body.desc);
											if(!req.body.desc)
												longTextModel.remove().then(callback, _fail);
											else
												longTextModel.save().then(function(textRecord){
													if(record.field('desc') != textRecord.field('id')){
														record.field('desc', textRecord.field('id'));
														record.save().then(callback, _fail);
													}else
														callback();
												}, _fail);
										};

										/* update chain */
										handleCover(
											function(coverid){
												dictionaryModel.field('title', req.body.title);
												dictionaryModel.field('author', req.body.author);
												dictionaryModel.field('pubYear', req.body.pubYear);
												dictionaryModel.field('cover', coverid);
												dictionaryModel.field('state', req.body.state);
												if(!description)
													dictionaryModel.field('desc', null);
												dictionaryModel.save().then(
													function(){
														handleLangs(
															function(){
																handleCategories(
																	function(){
																		handleDescription(
																			function(){
																				send(conn, res, null);
																			}, fail(res, conn, consts.e.ERR_DB_ERROR, 200));
																	}, fail(res, conn, consts.e.ERR_DB_ERROR, 200));
															}, fail(res, conn, consts.e.ERR_DB_ERROR, 200));
													}, fail(res, conn, consts.e.ERR_DB_ERROR));
											}, fail(res, conn, consts.e.ERR_FILE_ERROR));
									}, fail(res, conn, consts.e.ERR_MISSING_RECORD));
							}, fail(res, conn, consts.e.ERR_MISSING_RECORD));
					}, fail(res, conn, consts.e.ERR_MISSING_RECORD));
			}, fail(res, conn, consts.e.ERR_MISSING_RECORD));
	});
});

/**
 * Deletes a book.
 * A uuid with format of .{8}(-.{4}){3}-.{12} is required.
 */
router.delete('/:uuid', authenticate, access('dictionary:delete-uuid', 'حذف فرهنگ'), function(req, res, next) {
	if(!/.{8}(-.{4}){3}-.{12}/.exec(req.params.uuid))
		return fail(res, null, consts.e.ERR_MALFORMED_REQUEST)();

	db.getConnection(function(err, conn){
		if(err) return fail(res, conn, err)();

		/* load the book (check if it exists and get the id instead of uuid) */
		var dictionaryModel = new DictionaryModel(conn, {uuid: req.params.uuid});
		dictionaryModel.load().then(
			function(record) {
				if(!record)
					return fail(res, conn, consts.e.ERR_MISSING_RECORD)();
				else
					/* remove the book */
					record.remove().then(
						function(record) {
							if(record.field('cover')) {
								glob(path.join(BASEDIR, 'private/cover/'+record.field('cover')+'*'), {}, function(err, files){
									files.forEach(function(file){
										fs.unlinkSync(file);
									});
									send(conn, res, null);
								});
							}else
								send(conn, res, null);
						}, fail(res, conn, consts.e.ERR_DB_ERROR));
			}, fail(res, conn, consts.e.ERR_MISSING_RECORD));
	});
});

module.exports = router;