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/entry.js
/**
 * Express endpoints for /entry address
 * Version: 0.1
 * Author: Babak Vandad
 *
 * Restful api for addresses:
 *     GET      /entry
 *     PUT      /entry
 *     POST    /entry
 *     DELETE    /entry
 */

'use strict';
var express = require('express');
var router = express.Router();
var fs = require('fs');
var path = require('path');
var consts = require(path.join(BASEDIR, 'consts'));
var authHelper = require(path.join(BASEDIR, 'helpers/auth'));
var LongTextModel = require(path.join(BASEDIR, 'models/longText'));
var DictionaryModel = require(path.join(BASEDIR, 'models/dictionary'));
var UserAccessModel = require(path.join(BASEDIR, 'models/userAccess'));
var SubscriptionModel = require(path.join(BASEDIR, 'models/subscription'));
var EntryModel = require(path.join(BASEDIR, 'models/entry'));
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(res, conn, data){
  if(conn) conn.release();
  if(data) res.json(data);
  else res.end();
};

/**
 * makes a searchable string (removes spaces)
 * @param  {string} str user input or given entry title
 * @return {string}     searchable string
 */
var simplifyString = function(str){
  return str
    .trim()
    .toLowerCase()
    .replace(/['!\- ()]/g, '')
    .replace(/[ًٌٍَُِّْـ.]/g, '')
    .replace(/[‌ ]/g, '')
    .replace(/ي/g, 'ی')
    .replace(/ى/g, 'ی')
    .replace(/ك/g, 'ک')
    .replace(/[àâä]/g, 'a')
    .replace(/æ/g, 'ae')
    .replace(/[îï]/g, 'i')
    .replace(/[éèêë]/g, 'e')
    .replace(/[ôö]/g, 'o')
    .replace(/œ/g, 'oe')
    .replace(/[ùûü]/g, 'u')
    .replace(/ç/g, 'c')
    .replace(/ñ/g, 'n')
    .replace(/ÿ/g, 'y')
    .replace()
    ;
};

/** 
 * get the list of entries based on constraints:
 * return unique titles.
 * user subscriptions are considered.
 *      source      uuid
 *      title      
 *      langFrom
 *      langTo
 *      order
 *      limit      page size
 *      page      page number (from 0)
 */
router.get('/distinct', authenticate, access('entry:get-distinct', 'نمایش مدخل‌های یکتا از میان اشتراک‌های کاربر'), async (req, res, next) => {
  // return send(res, null, [req.user.fields(), req.user.hitData, req.user.subscriptions]);
  let conn;
  try {
    conn = await db.getConnectionPromise();
  }catch(err) { return fail(res, conn, consts.e.ERR_DB_ERROR)(); }

  var query = {};

  if(req.query.hasOwnProperty('langFrom')) query.langFrom = parseInt(req.query.langFrom);
  if(req.query.hasOwnProperty('langTo')) query.langTo = parseInt(req.query.langTo);
  if(req.query.hasOwnProperty('order')) query.order = parseInt(req.query.order);

  let reqSources = [];
  if(req.query.hasOwnProperty('source')) reqSources = req.query.source.split(',');
  let subSources = req.user.subscriptions.map(subscription => subscription.book);
  
  //admin is not checked for it's subscriptions
  // if(req.user.field('role')==consts.v.ROLE_ADMIN){
    if(reqSources.length)
      query.source = reqSources;
  /*}else{
    //user's can look up in dictionaries present both in subscriptions and in request
    if(reqSources.length)
      query.source = reqSources.filter(source => subSources.indexOf(source)!=-1);
    else
      query.source = subSources;

    if(!query.source.length)
      return send(res, conn, {sum: 0, list: []});
  }*/


    if(req.query.hasOwnProperty('title'))
      query['%%searchable'] = simplifyString(req.query.title)+'%';

    var limit = req.query.hasOwnProperty('limit') ? Math.min(100, parseInt(req.query.limit)) : 15;
    var page = req.query.hasOwnProperty('page') ? parseInt(req.query.page) : 0;
    var offset = page*limit;

    // list of records based on criteria
    var listQuery = function(records) {
      var response = [];
      EntryModel.countDistinct(conn, query).then(
        function(sum) {
          send(res, conn, {sum: sum, list: records.map(record => {
            delete record.data; /* data is only the id of long_text table. not useful for the user! */
            return record;
          })});
        }, fail(res, conn, consts.e.ERR_DB_ERROR));
    };


    EntryModel.listDistinct(conn, query, limit, offset, req.query.orderBy).then(
      listQuery, fail(res, conn, consts.e.ERR_DB_ERROR));
});

/** 
 * get the list of entries based on constraints:
 * return unique titles. each entry contains it's defenitions.
 * user subscriptions are considered.
 *      source      uuid
 *      title      
 *      langFrom
 *      langTo
 *      order
 *      limit      page size
 *      page      page number (from 0)
 */
router.get('/distinct/full', authenticate, access('entry:get-distinct', 'نمایش مدخل‌های یکتا از میان اشتراک‌های کاربر'), async (req, res, next) => {
  let conn;
  try {
    conn = await db.getConnectionPromise();
  }catch(err) { return fail(res, conn, consts.e.ERR_DB_ERROR)(); }

  var query = {};

  if(req.query.hasOwnProperty('langFrom')) query.langFrom = parseInt(req.query.langFrom);
  if(req.query.hasOwnProperty('langTo')) query.langTo = parseInt(req.query.langTo);
  if(req.query.hasOwnProperty('order')) query.order = parseInt(req.query.order);

  let reqSources = [];
  if(req.query.hasOwnProperty('source')) reqSources = req.query.source.split(',');
  let subSources = [];
  subSources = req.user.subscriptions.map(subscription => subscription.book);
  
  //admin is not checked for it's subscriptions
  // if(req.user.field('role')==consts.v.ROLE_ADMIN){
    if(reqSources.length)
      query.source = reqSources;
  /*}else{
    //user's can look up in dictionaries present both in subscriptions and in request
    if(reqSources.length)
      query.source = reqSources.filter(source => subSources.indexOf(source)!=-1);
    else
      query.source = subSources;
  }*/

  if(req.query.hasOwnProperty('title'))
    query['searchable'] = simplifyString(req.query.title);

  var limit = req.query.hasOwnProperty('limit') ? Math.min(100, parseInt(req.query.limit)) : -1;
  var page = req.query.hasOwnProperty('page') ? parseInt(req.query.page) : 0;
  var offset = page*limit;

  try {
    let records = await EntryModel.list(conn, query, -1, 0, req.query.orderBy, true);
    if(req.user.field('role')!=consts.v.ROLE_ADMIN)
      records = records.map(record => {
        if(subSources.indexOf(record.source)==-1)
          record.data = null;
        return record;
      });
    send(res, conn, {sum: records.length, list: records});
  }catch (e) {
    fail(res, conn, consts.e.ERR_DB_ERROR)();
  }
});

/** 
 * get the list of entries based on constraints:
 * user subscriptions are not considered. (only editors and admins must be allowed.)
 *      source      uuid
 *      title      
 *      langFrom
 *      langTo
 *      order
 *      limit      page size
 *      page      page number (from 0)
 *      full      load one full record not the list.
 *      neighbours    get next and previous pages (parameter full must be set)
 */
router.get('/', authenticate, access('entry:get', 'نمایش مدخل‌ها (بدون احتساب اشتراک‌های کاربر)'), function(req, res, next){
  db.getConnection(function(err, conn){
    if(err) return fail(res, conn, err)();

    var query = {};

    if(req.query.hasOwnProperty('langFrom')) query.langFrom = parseInt(req.query.langFrom);
    if(req.query.hasOwnProperty('langTo')) query.langTo = parseInt(req.query.langTo);
    if(req.query.hasOwnProperty('order')) query.order = parseInt(req.query.order);
    if(req.query.hasOwnProperty('source')) query.source = req.query.source;

    if(req.query.full && req.query.full=='false') req.query.full=false;
    if(req.query.neighbours && req.query.neighbours=='false') req.query.neighbours=false;

    if(req.query.hasOwnProperty('title')){
      if(req.query.full)
        query.title = req.query.title;
      else
        query['%%searchable'] = simplifyString(req.query.title)+'%';
    }

    var limit = req.query.hasOwnProperty('limit') ? Math.min(100, parseInt(req.query.limit)) : 15;
    var page = req.query.hasOwnProperty('page') ? parseInt(req.query.page) : 0;
    var offset = page*limit;

    // get next and previous records
    var getNeighbours = function(record, orderBy){
      var entryModel = new EntryModel(conn, record);
      if(!orderBy) orderBy='a-z';
      entryModel.getNeighbours(orderBy).then(
        function(record){
          send(res, conn, record.fields());
        }, fail(res, conn, consts.e.ERR_DB_ERROR));
    };

    // load the full record:
    // calculate maxOrder (override maxOrder field), data from longText, neighbours if set
    // TODO: get multiple records with full data. not only one.
    var loadFull = function(record){
      EntryModel.count(conn, {
        source: record.source,
        langFrom: record.langFrom,
        langTo: record.langTo,
        title: record.title,
      }).then(
        function(sum){
          record.maxOrder = sum;
          var longTextModel = new LongTextModel(conn, {id: record.data});
          longTextModel.load().then(
            function(longText) {
              record.data = JSON.parse(longText.field('content'));
              if(req.query.neighbours)
                getNeighbours(record, req.query.orderBy);
              else
                send(res, conn, record);
            }, fail(res, conn, consts.e.ERR_DB_ERROR));
        }, fail(res, conn, consts.e.ERR_DB_ERROR));
    };

    // list of records based on criteria
    var listQuery = function(records) {
      var response = [];
      if(!req.query.full)
        return EntryModel.count(conn, query).then(
          function(sum) {
            send(res, conn, {sum: sum, list: records.map(record => {
              delete record.data; /* data is only the id of long_text table. not useful for the user! */
              return record;
            })});
          }, fail(res, conn, consts.e.ERR_DB_ERROR));

      if(!records.length)  return send(res, conn, []);

      var record = records[0];
      loadFull(record);
    };

    EntryModel.list(conn, query, limit, offset, req.query.orderBy).then(
      listQuery, fail(res, conn, consts.e.ERR_DB_ERROR));
  });
});

/* add a new netry */
router.put('/', authenticate, access('entry:put', 'افزودن مدخل'), async (req, res, next) => {
  if(!req.body.title || !req.body.source || !req.body.langFrom || !req.body.langTo)
    return fail(res, null, consts.e.ERR_REQUIRED_FIELDS)();

  let conn;
  try { conn = await db.getConnectionPromise(); }
  catch(err) { return fail(res, conn, consts.e.ERR_DB_ERROR)(); }

  let dictionaryModel = new DictionaryModel(conn, {uuid: req.body.source});
  let book;
  try { book = await dictionaryModel.load(conn); }
  catch(err) { return fail(res, conn, consts.e.ERR_DB_ERROR)(); }

  let records;
  try { records = await EntryModel.list(conn, {
          title: req.body.title,
          source: req.body.source,
          langFrom: req.body.langFrom,
          langTo: req.body.langTo
        }) }
  catch(err) { return fail(res, conn, consts.e.ERR_DB_ERROR)(); }

  let newOrder = records ? records.length+1 : 1;
  let longTextModel = new LongTextModel(conn, {content: JSON.stringify(req.body.data)});
  try { await longTextModel.save(); }
  catch(err) { return fail(res, conn, consts.e.ERR_DB_ERROR)(); }

  let entryData = {
    title: req.body.title,
    searchable: simplifyString(req.body.title),
    source: book.field('id'),
    langFrom: req.body.langFrom,
    langTo: req.body.langTo,
    order: newOrder,
    maxOrder: newOrder,
    prefix: req.body.prefix,
    postfix: req.body.postfix,
    dataType: req.body.dataType
  };

  if(req.body.hasOwnProperty('pronunciation')){
    try {
      fs.renameSync(
        path.join(BASEDIR, 'private/temp/'+req.body.pronunciation),
        path.join(BASEDIR, 'private/pronunciation/'+req.body.pronunciation)
      );
      
      entryData.pronunciation = req.body.pronunciation;
    }catch(err) {}
  }

  let entryModel = new EntryModel(conn, entryData);
  entryModel.field('data', longTextModel.field('id'));
  let record;
  try {
    record = await entryModel.save(); 
    await record.setMaxOrder();
  }
  catch(err) { return fail(res, conn, consts.e.ERR_DB_ERROR)(); }
  send(res, conn, record.fields());
});

/* update an existing entry */
router.post('/', authenticate, access('entry:post', 'ویرایش مدخل'), async (req, res, next) => {
  if(!req.body.title || !req.body.source || !req.body.langFrom || !req.body.langTo || !req.body.order)
    return fail(res, null, consts.e.ERR_REQUIRED_FIELDS)();

  let conn;
  try {
    conn = await db.getConnectionPromise();
  }catch(err) { return fail(res, conn, consts.e.ERR_DB_ERROR)(); }

  req.body.reorder = Math.max(1, req.body.reorder);

  // we load all the records regardless of the order. maybe we need to reorder them.
  let records;
  try {
    records = await EntryModel.list(conn, {
      title: req.body.title,
      source: req.body.source,
      langFrom: req.body.langFrom,
      langTo: req.body.langTo,
    });
  }catch(e) {
    return fail(res, conn, consts.e.ERR_DB_ERROR)();
  }

  let record;
  let recordIndex;

  // get the proper record and it's index
  records.forEach(function(row, i){
    if(row.order == req.body.order) {
      record = row;
      recordIndex = i;
    }
  });

  // invalid record
  if(!record)
    return fail(res, conn, consts.e.ERR_MISSING_RECORD)();

  req.body.reorder = Math.min(req.body.reorder, records.length);

  // what if there is no longtext record yet? inconsitency
  let longTextModel = new LongTextModel(conn, {id: record.data});
  try {
    await longTextModel.load();
    // update longtext
    longTextModel.field('content', JSON.stringify(req.body.data));
    await longTextModel.save();
  }catch(err) { return fail(res, conn, consts.e.ERR_DB_ERROR)(); }


  let entryModel = new EntryModel(conn, {id: record.id});
  try {
    await entryModel.visit();
  }catch(err) { return fail(res, conn, consts.e.ERR_DB_ERROR)(); }

  let checkOrders = async () => {
    if(record.maxOrder==records.length && (!req.body.reorder || req.body.reorder==record.order))
      return;

    records.splice(recordIndex, 1);
    records.sort((a, b) => {
      if(a.order > b.order) return 1;
      if(a.order < b.order) return -1;
      if(a.order == b.order) return 0;
    });
    records.splice(req.body.reorder-1, 0, record);
    let reorders = [];
    records.forEach((record, i) => {
      if(record.order != i+1 || record.maxOrder!=records.length)
        reorders.push({id: record.id, order: i+1, maxOrder: records.length});
    });

    req.body.order = req.body.reorder;
    for(let i=0; i<reorders.length; i++)
      await EntryModel.reorder(conn, reorders[i].id, reorders[i].order, reorders[i].maxOrder);
  };

  var updateObj = {};
  if(req.body.hasOwnProperty('prefix') && req.body.prefix!=record.prefix)
    updateObj.prefix = req.body.prefix;
  if(req.body.hasOwnProperty('postfix') && req.body.postfix!=record.postfix)
    updateObj.postfix = req.body.postfix;
  if(req.body.hasOwnProperty('dataType') && req.body.dataType!=record.dataType)
    updateObj.dataType = req.body.dataType;
  if(req.body.hasOwnProperty('pronunciation') && req.body.pronunciation!=record.pronunciation){
    try {
      fs.unlinkSync(path.join(BASEDIR, 'private/pronunciation/'+record.pronunciation));
    }catch(err) {}
    if(req.body.pronunciation)
      try {
        fs.renameSync(
          path.join(BASEDIR, 'private/temp/'+req.body.pronunciation),
          path.join(BASEDIR, 'private/pronunciation/'+req.body.pronunciation)
        );
      
      updateObj.pronunciation = req.body.pronunciation;
    }catch(err) {}
    if(!req.body.pronunciation) updateObj.pronunciation='';
  }
  if(Object.keys(updateObj).length)
    await entryModel.update(updateObj);

  try {
    await checkOrders();
    send(res, conn, req.body);
  }catch(err) { return fail(res, conn, consts.e.ERR_DB_ERROR)(); }
});

/* delete an entry */
router.delete('/', authenticate, access('entry:delete', 'حذف مدخل'), async (req, res, next) => {
  if(!req.query.title || !req.query.source || !req.query.langFrom || !req.query.langTo || !req.query.order)
    return fail(res, null, consts.e.ERR_REQUIRED_FIELDS)();

  let conn;
  try { conn = await db.getConnectionPromise(); }
  catch(err) { return fail(res, conn, consts.e.ERR_DB_ERROR)(); }


  let records;
  try { records = await EntryModel.list(conn, {
      title: req.query.title,
      source: req.query.source,
      langFrom: req.query.langFrom,
      langTo: req.query.langTo,
    }); }
  catch(err) { return fail(res, conn, consts.e.ERR_DB_ERROR)(); }

  if(!records.length)
    return fail(res, conn, consts.e.ERR_MISSING_RECORD)();

  let record;
  let recordIndex;
  records.forEach(function(row, i){
    if(row.order == req.query.order) {
      record = row;
      recordIndex = i;
    }
  });

  if(!record)
    return fail(res, conn, consts.e.ERR_MISSING_RECORD)();

  let entryModel = new EntryModel(conn, {id: record.id});
  try { await entryModel.remove(); }
  catch(err) { return fail(res, conn, consts.e.ERR_DB_ERROR)(); }

  records.splice(recordIndex, 1);
  records.sort(function(a, b){
    if(a.order > b.order) return 1;
    if(a.order < b.order) return -1;
    if(a.order == b.order) return 0;
  });
  let reorders = [];
  records.forEach(function(record, i){
    reorders.push({id: record.id, order: i+1, maxOrder: records.length});
  });

  try {
    for(let i=0; i<reorders.length; i++)
      await EntryModel.reorder(conn, reorders[i].id, reorders[i].order, reorders[i].maxOrder);
  } catch(err) {}

  if(record.pronunciation)
    try {
      fs.unlinkSync(path.join(BASEDIR, 'private/pronunciation/'+record.pronunciation));
    }catch(err) {}

  send(res, conn, null);
});

module.exports = router;