diff --git a/routes/application.js b/routes/application.js index e87e5539c..ddcacf6a2 100755 --- a/routes/application.js +++ b/routes/application.js @@ -700,7 +700,7 @@ router.get('/list', block_access.isLoggedIn, function(req, res) { // Get user project for clone url generation let gitlabProjects = []; - if(gitlabConf.doGit) + if(gitlabConf.doGit && req.session.gitlab && req.session.gitlab.user) gitlabProjects = await gitlab.getAllProjects(req.session.gitlab.user.id); for (var i = 0; i < projects.length; i++) { diff --git a/routes/routes.js b/routes/routes.js index fc78da93c..5b9d50a7f 100755 --- a/routes/routes.js +++ b/routes/routes.js @@ -352,9 +352,10 @@ router.post('/reset_password_form', block_access.loginAccess, function(req, res) } }); + let gitlabUser = null; // Update Gitlab password if(gitlabConf.doGit){ - let gitlabUser = await gitlab.getUser(email_user); + gitlabUser = await gitlab.getUser(email_user); if(!gitlabUser) console.warn('Cannot update gitlab user password, user not found.'); @@ -373,10 +374,11 @@ router.post('/reset_password_form', block_access.loginAccess, function(req, res) } }); - return connectedUser; + return {connectedUser, gitlabUser}; - })().then(connectedUser => { - req.login(connectedUser, err => { + })().then(infos => { + + req.login(infos.connectedUser, err => { if (err) { console.error(err); req.session.toastr = [{ @@ -385,6 +387,9 @@ router.post('/reset_password_form', block_access.loginAccess, function(req, res) }]; res.redirect('/login'); } else { + req.session.gitlab = { + user: infos.gitlabUser + }; req.session.toastr = [{ message: "login.passwordReset", level: "success" diff --git a/services/bot.js b/services/bot.js index 8998683a6..f5fc5a4ba 100644 --- a/services/bot.js +++ b/services/bot.js @@ -2814,29 +2814,29 @@ exports.complete = function (instruction) { let answer = " "; let valid = true; - let letiable = false; + let variable = false; while ((m < l) && (k < n) && (valid)) { // Check if words are the same, goto next word if (template[k] == "(.*)" || template[k] == instr[m]) { - letiable = false; + variable = false; k++; } else { // Check if beginning of word are the same let sublen = instr[m].length; if (template[k].substring(0, sublen) == instr[m]) { // Do not increment k, we are still on keyword - letiable = false; + variable = false; } else { - // If we parse the letiable value + // If we parse the variable value if (template[k] == "(.*)") { // Check next word if (template[k + 1]) { k++; - letiable = true; + variable = true; } } else { - // If we are not parsing a letiable, it means template is not appropriate => Exit - if (!letiable) + // If we are not parsing a variable, it means template is not appropriate => Exit + if (!variable) valid = false; } } @@ -2857,12 +2857,12 @@ exports.complete = function (instruction) { else { if (template[k - 1] == "type") answer = answer + "[type] "; - // Return [letiable] to explain this is something dynamic + // Return [variable] to explain this is something dynamic else - answer = answer + "[letiable] "; + answer = answer + "[variable] "; - // If first loop on letiable, we need to display possible end of instruction - // Else, it means we have keyword at the beginning of suggestion, so we cut on letiable step + // If first loop on variable, we need to display possible end of instruction + // Else, it means we have keyword at the beginning of suggestion, so we cut on variable step if (!firstLoop) found = true; } diff --git a/services/designer.js b/services/designer.js index bddcbd7a3..20b481d0f 100755 --- a/services/designer.js +++ b/services/designer.js @@ -1046,7 +1046,7 @@ exports.setFieldKnownAttribute = function (attr, callback) { }).catch(function (err) { if (typeof err.parent !== "undefined" && (err.parent.errno == 1062 || err.parent.code == 23505)) { let err = new Error("structure.field.attributes.duplicateUnique"); - } else if(typeof err.parent !== "undefined" && (err.parent.errno == 1146 || err.parent.code == "42P01")){ + } else if(typeof err.parent !== "undefined" && (err.parent.errno == 1146 || err.parent.code == "42P01" || err.parent.code == "3F000")){ // Handle case by Newmips, no worry about this one if(['e_group', 'e_role', 'e_user'].indexOf(attr.name_data_entity) == -1 && attr.options.showValue == 'label'){ // Table do not exist - In case of script it's totally normal, just generate a warning @@ -3336,6 +3336,7 @@ exports.createWidgetLastRecords = function (attr, callback) { if (attr.columns[k].toLowerCase() == 'id') { attr.columns[k] = {codeName: 'id', name: 'id', found:true}; kFound = true; + continue; } for (var i = 0; i < columns.length; i++) { if (columns[i].codeName.indexOf('s_') == 0) diff --git a/structure/pieces/administration/routes/e_user.js b/structure/pieces/administration/routes/e_user.js index 6b75e80ef..83fc96db0 100644 --- a/structure/pieces/administration/routes/e_user.js +++ b/structure/pieces/administration/routes/e_user.js @@ -522,7 +522,8 @@ router.get('/set_status/:id_user/:status/:id_new_status', block_access.actionAcc status_helper.setStatus('e_user', req.params.id_user, req.params.status, req.params.id_new_status, req.session.passport.user.id, req.query.comment).then(()=> { res.redirect(req.headers.referer); }).catch((err)=> { - entity_helper.error(err, req, res, '/user/show?id=' + req.params.id_user, "e_user"); + req.session.toastr.push({level: 'error', message: 'component.status.error.action_error'}); + res.redirect(req.headers.referer); }); }); diff --git a/structure/pieces/component/document_template/routes/e_document_template.js b/structure/pieces/component/document_template/routes/e_document_template.js index cf4e663d3..48dcff29a 100644 --- a/structure/pieces/component/document_template/routes/e_document_template.js +++ b/structure/pieces/component/document_template/routes/e_document_template.js @@ -227,7 +227,8 @@ router.get('/set_status/:id_document_template/:status/:id_new_status', block_acc status_helper.setStatus('e_document_template', req.params.id_document_template, req.params.status, req.params.id_new_status, req.session.passport.user.id, req.query.comment).then(() => { res.redirect('/document_template/show?id=' + req.params.id_document_template); }).catch((err) => { - entity_helper.error(err, req, res, '/document_template/show?id=' + req.params.id_document_template); + req.session.toastr.push({level: 'error', message: 'component.status.error.action_error'}); + res.redirect(req.headers.referer); }); }); diff --git a/structure/pieces/component/status/locales/global_locales_FR.json b/structure/pieces/component/status/locales/global_locales_FR.json index 7c6523a43..788d537da 100644 --- a/structure/pieces/component/status/locales/global_locales_FR.json +++ b/structure/pieces/component/status/locales/global_locales_FR.json @@ -3,7 +3,7 @@ "status": { "error": { "illegal_status": "Ce statut n'est pas valide.", - "action_error": "Une ou plusieurs actions n'ont pas pu être éxecutée. Verifiez que les notifications sont activées et verifiez votre configuration mail." + "action_error": "Une ou plusieurs actions n'ont pas pu être executée. Verifiez que les notifications sont activées et verifiez votre configuration mail." }, "next_status": "Statut suivant", "available_status": "Statuts disponibles", diff --git a/structure/pieces/component/status/models/e_media_notification.js b/structure/pieces/component/status/models/e_media_notification.js index 01967e963..354ae8bbe 100644 --- a/structure/pieces/component/status/models/e_media_notification.js +++ b/structure/pieces/component/status/models/e_media_notification.js @@ -147,7 +147,7 @@ module.exports = (sequelize, DataTypes) => { } getGroupAndUserID().then(function(targetIds) { - var entityUrl; + var entityUrl, notificationObj; try { try { // Build show url of targeted entity @@ -161,7 +161,7 @@ module.exports = (sequelize, DataTypes) => { // Will redirect to current page entityUrl = '#'; } - var notificationObj = { + notificationObj = { f_color: self.f_color, f_icon: insertVariablesValue('f_icon'), f_title: insertVariablesValue('f_title'), diff --git a/structure/pieces/component/status/routes/e_action.js b/structure/pieces/component/status/routes/e_action.js index 70d36817c..2dca4fd9a 100644 --- a/structure/pieces/component/status/routes/e_action.js +++ b/structure/pieces/component/status/routes/e_action.js @@ -372,7 +372,8 @@ router.get('/set_status/:id_action/:status/:id_new_status', block_access.actionA status_helper.setStatus('e_action', req.params.id_action, req.params.status, req.params.id_new_status, req.session.passport.user.id, req.query.comment).then(()=> { res.redirect('/action/show?id=' + req.params.id_action) }).catch((err)=> { - entity_helper.error(err, req, res, '/action/show?id=' + req.params.id_action); + req.session.toastr.push({level: 'error', message: 'component.status.error.action_error'}); + res.redirect(req.headers.referer); }); }); diff --git a/structure/pieces/component/status/routes/e_media.js b/structure/pieces/component/status/routes/e_media.js index c97d0ab3a..94426a4db 100644 --- a/structure/pieces/component/status/routes/e_media.js +++ b/structure/pieces/component/status/routes/e_media.js @@ -280,7 +280,8 @@ router.get('/set_status/:id_media/:status/:id_new_status', block_access.actionAc status_helper.setStatus('e_media', req.params.id_media, req.params.status, req.params.id_new_status, req.session.passport.user.id, req.query.comment).then(()=> { res.redirect('/media/show?id=' + req.params.id_media); }).catch((err)=> { - entity_helper.error(err, req, res, '/media/show?id=' + req.params.id_media); + req.session.toastr.push({level: 'error', message: 'component.status.error.action_error'}); + res.redirect(req.headers.referer); }); }); diff --git a/structure/pieces/routes/data_entity.js b/structure/pieces/routes/data_entity.js index 1e6c3fdaa..da8da5ff9 100755 --- a/structure/pieces/routes/data_entity.js +++ b/structure/pieces/routes/data_entity.js @@ -524,7 +524,8 @@ router.get('/set_status/:id_ENTITY_URL_NAME/:status/:id_new_status', block_acces status_helper.setStatus('ENTITY_NAME', req.params.id_ENTITY_URL_NAME, req.params.status, req.params.id_new_status, req.session.passport.user.id, req.query.comment).then(()=> { res.redirect(req.headers.referer); }).catch(err => { - entity_helper.error(err, req, res, '/ENTITY_URL_NAME/show?id=' + req.params.id_ENTITY_URL_NAME, "ENTITY_NAME"); + req.session.toastr.push({level: 'error', message: 'component.status.error.action_error'}); + res.redirect(req.headers.referer); }); }); diff --git a/structure/structure_data_field.js b/structure/structure_data_field.js index 33b0c3d67..1781ca923 100755 --- a/structure/structure_data_field.js +++ b/structure/structure_data_field.js @@ -397,10 +397,14 @@ function getFieldHtml(type, nameDataField, nameDataEntity, readOnly, file, value str += " \n"; } else { str += "
\n"; - str += "
\n"; - str += " \n"; - str += "
\n"; - str += " {" + value2 + "|filename}\n"; + str += " {?" + value2 + "}\n"; + str += "
\n"; + str += " \n"; + str += "
\n"; + str += " {" + value2 + "|filename}\n"; + str += " {:else}\n"; + str += " {#__ key=\"message.empty_file\" /}\n"; + str += " {/" + value2 + "}\n"; str += "
\n"; } break; @@ -413,7 +417,7 @@ function getFieldHtml(type, nameDataField, nameDataEntity, readOnly, file, value str += " \n"; } else { str += "
\n"; - str += "  + value + \n"; + str += "  + value + \n"; str += "
\n"; } break; @@ -630,6 +634,7 @@ exports.setupDataField = function (attr, callback) { case "mel" : typeForModel = "STRING"; typeForDatalist = "email"; + type_data_field = "email"; break; case "phone" : case "tel" : diff --git a/structure/template/locales/en-EN.json b/structure/template/locales/en-EN.json index 62269af5c..787aebd32 100755 --- a/structure/template/locales/en-EN.json +++ b/structure/template/locales/en-EN.json @@ -173,6 +173,7 @@ "question": "Are you sure you want to delete this entity ?" }, "empty": "No data to display", + "empty_file": "No file", "unique": "This field must be unique:", "relatedtomanycheckbox_required": "A field requires you to check at least one checkbox." }, diff --git a/structure/template/locales/fr-FR.json b/structure/template/locales/fr-FR.json index 383e542aa..ff205ca9f 100755 --- a/structure/template/locales/fr-FR.json +++ b/structure/template/locales/fr-FR.json @@ -173,6 +173,7 @@ "question": "Êtes-vous sûr de vouloir supprimer cette entité ?" }, "empty": "Aucune donnée à afficher", + "empty_file": "Aucun fichier", "unique": "Ce champ doit être unique:", "relatedtomanycheckbox_required": "Un champ necessite d'avoir au moins une case cochée." }, diff --git a/structure/template/public/js/Newmips/dataTableBuilder.js b/structure/template/public/js/Newmips/dataTableBuilder.js index 30c8672b7..1d6db05ae 100644 --- a/structure/template/public/js/Newmips/dataTableBuilder.js +++ b/structure/template/public/js/Newmips/dataTableBuilder.js @@ -416,7 +416,7 @@ function init_datatable(tableID, doPagination, context) { valueFromArray = getValue(parts, row); } } else { - // Has one sur une sous entité + // Has one relation field let parts = columns[meta.col].data.split('.'); valueFromArray = getValue(parts, row); } @@ -440,7 +440,7 @@ function init_datatable(tableID, doPagination, context) { if (typeof columns[meta.col].type != 'undefined') { // date / datetime if (columns[meta.col].type == 'date' || columns[meta.col].type == 'datetime') { - if (cellValue != "" && cellValue != null && cellValue != "Invalid date" && cellValue != "Invalid Date") { + if (cellValue != null && cellValue != "" && cellValue.toLowerCase() != "invalid date") { var tmpDate = moment(new Date(cellValue)); if (!tmpDate.isValid()) cellValue = '-'; @@ -484,6 +484,9 @@ function init_datatable(tableID, doPagination, context) { // Get current entity by splitting current table id var currentEntity = tableID.split("#table_")[1]; var justFilename = cellValue.replace(cellValue.split("_")[0], "").substring(1); + // Remove uuid + if(justFilename[32] == '_') + justFilename = justFilename.substring(33); cellValue = ''+justFilename+''; } else cellValue = ''; @@ -609,7 +612,6 @@ function init_datatable(tableID, doPagination, context) { } ] } - // Global search table = $(tableID, context).DataTable(tableOptions); //modal on click on picture cell @@ -650,7 +652,6 @@ function init_datatable(tableID, doPagination, context) { var mainTh = $(this); var title = $(this).text(); - // Custom var currentField = mainTh.data('field'); var val = getFilter(tableID.substring(1), currentField); var search = ''; diff --git a/structure/template/public/js/newmips.js b/structure/template/public/js/newmips.js index 85c37966b..ee207cf2d 100755 --- a/structure/template/public/js/newmips.js +++ b/structure/template/public/js/newmips.js @@ -425,7 +425,7 @@ function initForm(context) { if (!confirm('Êtes-vous sûr de vouloir supprimer ce fichier ?')) return false; $.ajax({ - url: '/default/delete-file-ajax', + url: '/default/delete_file', type: 'post', data: { dataEntity: that.attr("data-entity"), diff --git a/structure/template/routes/default.js b/structure/template/routes/default.js index e12601e37..8ac7a7b14 100755 --- a/structure/template/routes/default.js +++ b/structure/template/routes/default.js @@ -294,6 +294,11 @@ router.get('/get_picture', block_access.isLoggedIn, function (req, res) { let entity = req.query.entity; let filename = req.query.src; let cleanFilename = filename.substring(16); + + // Remove uuid + if(cleanFilename[32] == '_') + cleanFilename = cleanFilename.substring(33); + let folderName = filename.split("-")[0]; let filePath = globalConfig.localstorage + entity + '/' + folderName + '/' + filename; @@ -322,6 +327,11 @@ router.get('/download', block_access.isLoggedIn, function (req, res) { let entity = req.query.entity; let filename = req.query.f; let cleanFilename = filename.substring(16); + + // Remove uuid + if(cleanFilename[32] == '_') + cleanFilename = cleanFilename.substring(33); + let folderName = filename.split("-")[0]; let filePath = globalConfig.localstorage + entity + '/' + folderName + '/' + filename; @@ -345,37 +355,34 @@ router.get('/download', block_access.isLoggedIn, function (req, res) { } }); -router.post('/delete-file-ajax', block_access.isLoggedIn, function (req, res) { - let entity = req.body.dataEntity; - let dataStorage = req.body.dataStorage; - let filename = req.body.filename; - if (entity && dataStorage && filename) { - let partOfFilepath = filename.split('-'); - if (partOfFilepath.length) { - let base = partOfFilepath[0]; - let completeFilePath = globalConfig.localstorage + entity + '/' + base + '/' + filename; - // thumbnail file to delete - let completeThumbnailPath = globalConfig.localstorage + globalConfig.thumbnail.folder + entity + '/' + base + '/' + filename; - fs.unlink(completeFilePath, function (err) { - if (!err) { - res.status(200).json({ message: 'message.delete.success'}); - fs.unlink(completeThumbnailPath, function (err) { - if (err) - console.error(err); - }); - } else { - req.session.toastr.push({level: 'error', message: "Internal error"}); - res.status(500).json({message: ''}); - } - }); - } else { - req.session.toastr.push({level: 'error', message: "File syntax not valid"}); - res.status(404).json({message: ''}); - } +router.post('/delete_file', block_access.isLoggedIn, function (req, res) { + try { + let entity = req.body.dataEntity; + let filename = req.body.filename; + let cleanFilename = filename.substring(16); + + // Remove uuid + if(cleanFilename[32] == '_') + cleanFilename = cleanFilename.substring(33); + + let folderName = filename.split("-")[0]; + let filePath = globalConfig.localstorage + entity + '/' + folderName + '/' + filename; + + if (!block_access.entityAccess(req.session.passport.user.r_group, entity.substring(2))) + throw new Error("403 - Access forbidden"); + + if (!fs.existsSync(filePath)) + throw new Error("404 - File not found: " + filePath); - } else { - req.session.toastr.push({level: 'error', message: "File not found"}); - res.status(400).json({message: 'Request parameters must be set'}); + fs.unlinkSync(filePath); + res.status(200).send(true); + } catch (err) { + console.error(err); + req.session.toastr.push({ + level: 'error', + message: "error.500.file" + }); + res.status(500).send(err); } }); diff --git a/structure/template/server.js b/structure/template/server.js index 70944db66..5c631a43a 100755 --- a/structure/template/server.js +++ b/structure/template/server.js @@ -350,24 +350,21 @@ models.sequelize.sync({logging: false, hooks: false}).then(() => { if (!users || users.length == 0 || !hasAdmin) { models.E_group.create({ - id: 1, version: 0, f_label: 'admin' - }).then(function() { + }).then(group => { models.E_role.create({ - id: 1, version: 0, f_label: 'admin' - }).then(function() { + }).then(role => { models.E_user.create({ - id: 1, f_login: 'admin', f_password: null, f_enabled: 0, version: 0 - }).then(function(user) { - user.setR_role(1); - user.setR_group(1); + }).then(user => { + user.setR_role(role.id); + user.setR_group(group.id); }); }); }); diff --git a/structure/template/utils/dust_fn.js b/structure/template/utils/dust_fn.js index 037c81884..eeab289a0 100644 --- a/structure/template/utils/dust_fn.js +++ b/structure/template/utils/dust_fn.js @@ -144,8 +144,13 @@ module.exports = { dust.filters.filename = function(value) { // Remove datetime part from filename display - if (moment(value.substring(0,16), 'YYYYMMDD-HHmmss_').isValid() && value != "" && value.length > 16) - return value.substring(16); + if (moment(value.substring(0, 16), 'YYYYMMDD-HHmmss_').isValid() && value != "" && value.length > 16) + value = value.substring(16); + + // Remove uuid + if(value[32] == '_') + value = value.substring(33); + return value; }; diff --git a/structure/template/utils/status_helper.js b/structure/template/utils/status_helper.js index e984b640a..762220dac 100644 --- a/structure/template/utils/status_helper.js +++ b/structure/template/utils/status_helper.js @@ -56,7 +56,7 @@ module.exports = { fullEntityFieldTree: function (entity, alias = entity) { let genealogy = []; // Create inner function to use genealogy globaly - function loadTree(entity, alias) { + function loadTree(entity, alias, depth = 0) { let fieldTree = { entity: entity, alias: alias, @@ -66,9 +66,10 @@ module.exports = { file_fields: [], children: [] } + let entityFields, entityAssociations; try { - let entityFields = JSON.parse(fs.readFileSync(__dirname+'/../models/attributes/'+entity+'.json')); - let entityAssociations = JSON.parse(fs.readFileSync(__dirname+'/../models/options/'+entity+'.json')); + entityFields = JSON.parse(fs.readFileSync(__dirname+'/../models/attributes/'+entity+'.json')); + entityAssociations = JSON.parse(fs.readFileSync(__dirname+'/../models/options/'+entity+'.json')); } catch (e) { console.error(e); return fieldTree; @@ -86,39 +87,124 @@ module.exports = { } // Check if current entity has already been built in this branch of the tree to avoid infinite loop - let foundGenealogy = genealogy.filter(x => x.entity == entity); - // Entity already proceeded in an other relation - if(foundGenealogy.length != 0){ - // Check for the better depth, if deeper then remove it to keep the closer one - if(foundGenealogy[0].depth > depth) - genealogy = genealogy.filter(x => x.entity != entity); // Remove old one - else - return fieldTree; - } + for (const [idx, genealogyBranch] of genealogy.entries()) + if (genealogyBranch.entity == entity) { + // Keep smallest depth + if (genealogyBranch.depth > depth) + genealogy.splice(idx, 1); + else + return fieldTree; + } genealogy.push({ entity: entity, depth: depth }); - let initalDepth = depth; - // Building children array - for (let i = 0; i < entityAssociations.length; i++){ + for (let i = 0; i < entityAssociations.length; i++) { // Do not include history & status table in field list - if(entityAssociations[i].target.indexOf("e_history_e_") == -1 - && entityAssociations[i].target.indexOf("e_status") == -1){ - depth++; - fieldTree.children.push(loadTree(entityAssociations[i].target, entityAssociations[i].as)); - } + if(entityAssociations[i].target.indexOf("e_history_e_") == -1 && entityAssociations[i].target.indexOf("e_status") == -1 && entityAssociations[i].structureType !== 'auto_generate') + fieldTree.children.push(loadTree(entityAssociations[i].target, entityAssociations[i].as, depth+1)); } - depth = initalDepth; - return fieldTree; } return loadTree(entity, alias); }, + // Build array of fields for media sms/notification/email insertion entityFieldForSelect: function(entityTree, lang) { var __ = language(lang).__; var separator = ' > '; var options = []; function dive(obj, codename, parent, parentTraduction = "") { var traduction; - // Component address have a different traduction naming policy. If obj is a address component, adapt traductions fetched // Top level. Entity traduction Ex: 'Ticket' if (!parent) traduction = __('entity.'+obj.entity+'.label_entity'); @@ -198,36 +282,45 @@ module.exports = { this.loopCount++; if (this.loopCount % 1000 === 0) { this.loopCount = 0; - return setTimeout(() => { - console.log(...args); - sortFunc(...args); - }, 0); + return setTimeout(() => {sortFunc(...args);}, 0); } return sortFunc(...args); } - function sort(optsArray, i = 0) { - if (i < 0) i = 0; - if (!optsArray[i+1]) + function swap(arr, i, j) { + const tmp = arr[j]; + arr[j] = arr[i]; + arr[i] = tmp; + } + function sort(array, idx = 0) { + if (idx < 0) idx = 0; + if (!array || !array[idx+1]) return; - var firstParts = optsArray[i].traduction.split(separator); - var secondParts = optsArray[i+1].traduction.split(separator); - if (firstParts[0].toLowerCase() > secondParts[0].toLowerCase()) { - var swap = optsArray[i+1]; - optsArray[i+1] = optsArray[i]; - optsArray[i] = swap; - i--; - } - else if (firstParts[0].toLowerCase() == secondParts[0].toLowerCase() - && firstParts[1].toLowerCase() > secondParts[1].toLowerCase()) { - var swap = optsArray[i+1]; - optsArray[i+1] = optsArray[i]; - optsArray[i] = swap; - i--; + + const first = array[idx].traduction.split(separator); + const second = array[idx+1].traduction.split(separator); + + // Swap because of depth difference + if (first.length > second.length) { + swap(array, idx, idx+1); + idx--; } + else if (first.length == second.length) + // Dive depth until mismatch + for (let i = 0; i < first.length; i++) { + if (first[i] > second[i]) { + swap(array, idx, idx+1); + idx--; + break; + } + else if (first[i] < second[i]) { + idx++; + break; + } + } else - i++; + idx++; - return stackProtectedRecursion(sort, optsArray, i); + stackProtectedRecursion(sort, array, idx); } sort(options);