From 6fd9ba20c8720119c84ac6537688f937f2379e03 Mon Sep 17 00:00:00 2001 From: Thilina Hasantha Date: Fri, 15 Apr 2016 15:53:14 +0530 Subject: [PATCH] Release note v15.3 ------------------ ### Fixes * Fix issue: classes should be loaded even the module is disabled * Deleting the only Admin user is not allowed * Fixes for handling non UTF-8 * Fix for non-mandatory select boxes are shown as mandatory --- core-ext/config.base.php | 6 +- core-ext/model/models.base.php | 10 +- readme.md | 8 + src/api/AdapterBase.js | 293 +++++++++++++ src/api/Base.js | 13 +- src/classes/AbstractModuleManager.php | 8 + src/classes/BaseService.php | 72 ++- src/classes/CronUtils.php | 8 +- src/classes/Macaw.php | 176 ++++++++ src/classes/RestApiManager.php | 49 ++- src/data.php | 13 +- src/include.common.php | 1 + src/js/mindmup-editabletable.js | 132 ++++++ src/logout.php | 2 +- src/model/models.base.php | 4 + src/model/models.inc.php | 11 + src/modules.php | 18 + src/rest.php | 39 +- src/server.includes.inc.php | 4 +- src/service.php | 32 +- src/utils/EvalMath.php | 608 ++++++++++++++++++++++++++ 21 files changed, 1447 insertions(+), 60 deletions(-) create mode 100755 src/classes/Macaw.php create mode 100644 src/js/mindmup-editabletable.js create mode 100644 src/utils/EvalMath.php diff --git a/core-ext/config.base.php b/core-ext/config.base.php index e4799011..b572a30d 100644 --- a/core-ext/config.base.php +++ b/core-ext/config.base.php @@ -9,9 +9,9 @@ define('HOME_LINK_ADMIN', CLIENT_BASE_URL."?g=admin&n=dashboard&m=admin_Admin"); define('HOME_LINK_OTHERS', CLIENT_BASE_URL."?g=modules&n=dashboard&m=module_Personal_Information"); //Version -define('VERSION', '15.2.OS'); -define('CACHE_VALUE', '15.2.OS'); -define('VERSION_DATE', '12/03/2016'); +define('VERSION', '15.3.OS'); +define('CACHE_VALUE', '15.3.OS'); +define('VERSION_DATE', '14/04/2016'); if(!defined('CONTACT_EMAIL')){define('CONTACT_EMAIL','icehrm@gamonoid.com');} if(!defined('KEY_PREFIX')){define('KEY_PREFIX','IceHrm');} diff --git a/core-ext/model/models.base.php b/core-ext/model/models.base.php index d841ba89..789ab786 100644 --- a/core-ext/model/models.base.php +++ b/core-ext/model/models.base.php @@ -61,9 +61,13 @@ class ICEHRM_Record extends ADOdb_Active_Record{ return $obj; } - public function getDefaultAccessLevel(){ - return array("get","element","save","delete"); - } + public function postProcessGetElement($obj){ + return $obj; + } + + public function getDefaultAccessLevel(){ + return array("get","element","save","delete"); + } public function getVirtualFields(){ return array( diff --git a/readme.md b/readme.md index 7f72632e..bcf84bda 100644 --- a/readme.md +++ b/readme.md @@ -532,6 +532,14 @@ That way you can attach each and every project to a client. Under employee projects tab you can assign projects to employees. You need to add projects to employees to enable them to add time against these projects in time-sheets. +Release note v15.3 +------------------ +### Fixes + * Fix issue: classes should be loaded even the module is disabled + * Deleting the only Admin user is not allowed + * Fixes for handling non UTF-8 + * Fix for non-mandatory select boxes are shown as mandatory + Release note v15.2 ------------------ diff --git a/src/api/AdapterBase.js b/src/api/AdapterBase.js index d00f4acb..07ee0f34 100644 --- a/src/api/AdapterBase.js +++ b/src/api/AdapterBase.js @@ -680,6 +680,299 @@ ApproveModuleAdapter.method('getActionButtonsHtml', function(id,data) { +/** + * TableEditAdapter + */ + +function TableEditAdapter(endPoint) { + this.initAdapter(endPoint); + this.cellDataUpdates = {}; + this.modulePath = ''; + this.rowFieldName = ''; + this.columnFieldName = ''; + this.rowTable = ''; + this.columnTable = ''; + this.valueTable = ''; + this.csvData = []; +} + +TableEditAdapter.inherits(AdapterBase); + +TableEditAdapter.method('setModulePath', function(path) { + this.modulePath = path; +}); + +TableEditAdapter.method('setRowFieldName', function(name) { + this.rowFieldName = name; +}); + +TableEditAdapter.method('setTables', function(rowTable, columnTable, valueTable) { + this.rowTable = rowTable; + this.columnTable = columnTable; + this.valueTable = valueTable; +}); + +TableEditAdapter.method('setColumnFieldName', function(name) { + this.columnFieldName = name; +}); + +TableEditAdapter.method('getDataMapping', function() { + return [ + ]; +}); + + +TableEditAdapter.method('getFormFields', function() { + return [ + ]; +}); + +TableEditAdapter.method('get', function() { + this.getAllData(); +}); + +TableEditAdapter.method('getAllData', function(save) { + var req = {}; + req.rowTable = this.rowTable; + req.columnTable = this.columnTable; + req.valueTable = this.valueTable; + req = this.addAdditionalRequestData('getAllData', req); + req.save = (save == undefined || save == null || save == false)?0:1; + var reqJson = JSON.stringify(req); + + var callBackData = []; + callBackData['callBackData'] = []; + callBackData['callBackSuccess'] = 'getAllDataSuccessCallBack'; + callBackData['callBackFail'] = 'getAllDataFailCallBack'; + + this.customAction('getAllData',this.modulePath,reqJson,callBackData); +}); + +TableEditAdapter.method('getDataItem', function(row,column,allData) { + var columnData = allData[1]; + var rowData = allData[0]; + var serverData = allData[2]; + + if(column == -1){ + return rowData[row].name; + }else{ + return this.getDataItemByKeyValues(this.rowFieldName, rowData[row].id, this.columnFieldName, columnData[column].id, serverData); + } +}); + +TableEditAdapter.method('getDataItemByKeyValues', function(rowKeyName, rowKeyVal, colKeyName, colKeyVal, data) { + for(var i=0;i
'; + + //Find current page + var activePage = $('#'+elementId +" .dataTables_paginate .active a").html(); + var start = 0; + if(activePage != undefined && activePage != null){ + start = parseInt(activePage, 10)*15 - 15; + } + + $('#'+elementId).html(html); + + var dataTableParams = { + "oLanguage": { + "sLengthMenu": "_MENU_ records per page" + }, + "aaData": data, + "aoColumns": headers, + "bSort": false, + "iDisplayLength": 15, + "iDisplayStart": start + }; + + + var customTableParams = this.getCustomTableParams(); + + $.extend(dataTableParams, customTableParams); + + $('#'+elementId+' #grid').dataTable( dataTableParams ); + + $(".dataTables_paginate ul").addClass("pagination"); + $(".dataTables_length").hide(); + $(".dataTables_filter input").addClass("form-control"); + $(".dataTables_filter input").attr("placeholder","Search"); + $(".dataTables_filter label").contents().filter(function(){ + return (this.nodeType == 3); + }).remove(); + //$('.tableActionButton').tooltip(); + $('#'+elementId+' #grid').editableTableWidget(); + + $('#'+elementId+' #grid .editcell').on('validate', function(evt, newValue) { + + return modJs.validateCellValue($(this), evt, newValue); + + }); +}); + +TableEditAdapter.method('addCellDataUpdate' , function(colId, rowId, data) { + + this.cellDataUpdates[colId+"="+rowId] = [colId, rowId, data]; +}); + +TableEditAdapter.method('addAdditionalRequestData' , function(type, req) { + return req; +}); + +TableEditAdapter.method('sendCellDataUpdates' , function() { + var req = this.cellDataUpdates; + req.rowTable = this.rowTable; + req.columnTable = this.columnTable; + req.valueTable = this.valueTable; + req = this.addAdditionalRequestData('updateData', req); + var reqJson = JSON.stringify(req); + + var callBackData = []; + callBackData['callBackData'] = []; + callBackData['callBackSuccess'] = 'updateDataSuccessCallBack'; + callBackData['callBackFail'] = 'updateDataFailCallBack'; + this.showLoader(); + this.customAction('updateData',this.modulePath,reqJson,callBackData); +}); + +TableEditAdapter.method('updateDataSuccessCallBack', function(callBackData,serverData) { + this.hideLoader(); + modJs.cellDataUpdates = {}; + modJs.get(); +}); + +TableEditAdapter.method('updateDataFailCallBack', function(callBackData,serverData) { + this.hideLoader(); +}); + +TableEditAdapter.method('sendAllCellDataUpdates' , function() { + + var req = this.cellDataUpdates; + req.rowTable = this.rowTable; + req.columnTable = this.columnTable; + req.valueTable = this.valueTable; + req = this.addAdditionalRequestData('updateAllData', req); + var reqJson = JSON.stringify(req); + + var callBackData = []; + callBackData['callBackData'] = []; + callBackData['callBackSuccess'] = 'updateDataAllSuccessCallBack'; + callBackData['callBackFail'] = 'updateDataAllFailCallBack'; + this.showLoader(); + this.customAction('updateAllData',this.modulePath,reqJson,callBackData); +}); + +TableEditAdapter.method('updateDataAllSuccessCallBack', function(callBackData,serverData) { + this.hideLoader(); + modJs.cellDataUpdates = {}; + modJs.getAllData(true); +}); + +TableEditAdapter.method('updateDataAllFailCallBack', function(callBackData,serverData) { + this.hideLoader(); +}); + +TableEditAdapter.method('showActionButtons' , function() { + return false; +}); + + + + + + /** * RequestCache diff --git a/src/api/Base.js b/src/api/Base.js index 2abfff35..b66547c0 100644 --- a/src/api/Base.js +++ b/src/api/Base.js @@ -1725,7 +1725,10 @@ IceHRMBase.method('fillForm', function(object, formId, fields) { } $(formId + ' #'+fields[i][0]).select2('val',msVal); - + var select2Height = $(formId + ' #'+fields[i][0]).find(".select2-choices").height(); + $(formId + ' #'+fields[i][0]).find(".controls").css('min-height', select2Height+"px"); + $(formId + ' #'+fields[i][0]).css('min-height', select2Height+"px"); + }else if(fields[i][1].type == 'datagroup'){ try{ var html = this.dataGroupToHtml(object[fields[i][0]],fields[i]); @@ -1764,7 +1767,13 @@ IceHRMBase.method('renderFormField', function(field) { } var t = this.fieldTemplates[field[1].type]; if(field[1].validation != "none" && field[1].validation != "emailOrEmpty" && field[1].validation != "numberOrEmpty" && field[1].type != "placeholder" && field[1].label.indexOf('*') < 0){ - field[1].label = field[1].label + '*'; + var tempSelectBoxes = ['select','select2']; + if(tempSelectBoxes.indexOf(field[1].type) >= 0 && field[1]['allow-null'] == true){ + + }else{ + field[1].label = field[1].label + '*'; + } + } if(field[1].type == 'text' || field[1].type == 'textarea' || field[1].type == 'hidden' || field[1].type == 'label' || field[1].type == 'placeholder'){ t = t.replace(/_id_/g,field[0]); diff --git a/src/classes/AbstractModuleManager.php b/src/classes/AbstractModuleManager.php index 94191c92..da265660 100644 --- a/src/classes/AbstractModuleManager.php +++ b/src/classes/AbstractModuleManager.php @@ -65,6 +65,10 @@ abstract class AbstractModuleManager{ */ public abstract function setupModuleClassDefinitions(); + public function initCalculationHooks(){ + + } + public function initQuickAccessMenu(){ } @@ -207,6 +211,10 @@ abstract class AbstractModuleManager{ $eh->Save(); } + + public function addCalculationHook($code, $name, $class, $method){ + BaseService::getInstance()->addCalculationHook($code, $name, $class, $method); + } } \ No newline at end of file diff --git a/src/classes/BaseService.php b/src/classes/BaseService.php index e56f2248..c41b2945 100644 --- a/src/classes/BaseService.php +++ b/src/classes/BaseService.php @@ -46,7 +46,8 @@ class BaseService{ var $emailSender = null; var $user = null; var $historyManagers = array(); - + var $calculationHooks = array(); + private static $me = null; private function __construct(){ @@ -131,10 +132,15 @@ class BaseService{ LogManager::getInstance()->debug("Query: "."1=1".$query.$orderBy); LogManager::getInstance()->debug("Query Data: ".print_r($queryData,true)); $list = $obj->Find("1=1".$query.$orderBy,$queryData); - } + } + + $newList = array(); + foreach($list as $listObj){ + $newList[] = $this->cleanUpAdoDB($listObj); + } if(!empty($mappingStr) && count($map)>0){ - $list = $this->populateMapping($list, $map); + $list = $this->populateMapping($newList, $map); } return $list; @@ -363,7 +369,7 @@ class BaseService{ $processedList = array(); foreach($list as $obj){ - $processedList[] = $obj->postProcessGetData($obj); + $processedList[] = $this->cleanUpAdoDB($obj->postProcessGetData($obj)); } $list = $processedList; @@ -488,7 +494,8 @@ class BaseService{ } } } - return $obj->postProcessGetData($obj); + $obj = $obj->postProcessGetElement($obj); + return $this->cleanUpAdoDB($obj->postProcessGetData($obj)); } return null; } @@ -685,6 +692,7 @@ class BaseService{ } foreach($list as $obj){ + $obj = $this->cleanUpAdoDB($obj); if(count($values) == 1){ $ret[$obj->$key] = $obj->$value; }else{ @@ -1258,6 +1266,60 @@ class BaseService{ return $obj; + } + + public function addCalculationHook($code, $name, $class, $method){ + $calcualtionHook = new CalculationHook(); + $calcualtionHook->code = $code; + $calcualtionHook->name = $name; + $calcualtionHook->class = $class; + $calcualtionHook->method = $method; + $this->calculationHooks[$code] = $calcualtionHook; + } + + public function getCalculationHooks(){ + return array_values($this->calculationHooks); + } + + public function getCalculationHook($code){ + return $this->calculationHooks[$code]; + } + + public function executeCalculationHook($parameters, $code = NULL){ + $ch = BaseService::getInstance()->getCalculationHook($code); + + if(empty($ch->code)){ + return null; + } + $class = $ch->class; + return call_user_func_array(array(new $class(), $ch->method), $parameters); + } + + public function cleanNonUTFChar($obj){ + $regex = <<<'END' +/ + ( + (?: [\x00-\x7F] # single-byte sequences 0xxxxxxx + | [\xC0-\xDF][\x80-\xBF] # double-byte sequences 110xxxxx 10xxxxxx + | [\xE0-\xEF][\x80-\xBF]{2} # triple-byte sequences 1110xxxx 10xxxxxx * 2 + | [\xF0-\xF7][\x80-\xBF]{3} # quadruple-byte sequence 11110xxx 10xxxxxx * 3 + ){1,100} # ...one or more times + ) +| . # anything else +/x +END; + if(is_string($obj)){ + return preg_replace($regex, '$1', $obj); + }else{ + + foreach($obj as $key => $val){ + + + $obj->$key = preg_replace($regex, '$1', $val); + } + return $obj; + } + } } diff --git a/src/classes/CronUtils.php b/src/classes/CronUtils.php index 78e7a58e..7117109c 100644 --- a/src/classes/CronUtils.php +++ b/src/classes/CronUtils.php @@ -102,7 +102,7 @@ class IceCron{ return true; } }else{ - if(intval(date('m')) <= intval($time) && date('H') != date('H',strtotime($lastRunTime))){ + if(intval(date('i')) <= intval($time) && date('H') != date('H',strtotime($lastRunTime))){ return true; } } @@ -113,7 +113,7 @@ class IceCron{ return true; } }else{ - if(intval(date('H')) <= intval($time) && date('d') != date('d',strtotime($lastRunTime))){ + if(intval(date('H')) >= intval($time) && date('d') != date('d',strtotime($lastRunTime))){ return true; } } @@ -124,7 +124,7 @@ class IceCron{ return true; } }else{ - if(intval(date('d')) <= intval($time) && date('m') != date('m',strtotime($lastRunTime))){ + if(intval(date('d')) >= intval($time) && date('m') != date('m',strtotime($lastRunTime))){ return true; } } @@ -134,7 +134,7 @@ class IceCron{ return true; } }else{ - if(intval(date('m')) <= intval($time) && date('Y') != date('Y',strtotime($lastRunTime))){ + if(intval(date('m')) >= intval($time) && date('Y') != date('Y',strtotime($lastRunTime))){ return true; } } diff --git a/src/classes/Macaw.php b/src/classes/Macaw.php new file mode 100755 index 00000000..be67a28e --- /dev/null +++ b/src/classes/Macaw.php @@ -0,0 +1,176 @@ + '[^/]+', + ':num' => '[0-9]+', + ':all' => '.*' + ); + + public static $error_callback; + + /** + * Defines a route w/ callback and method + */ + public static function __callstatic($method, $params) + { + + $uri = dirname($_SERVER['PHP_SELF']).$params[0]; + $callback = $params[1]; + + array_push(self::$routes, $uri); + array_push(self::$methods, strtoupper($method)); + array_push(self::$callbacks, $callback); + } + + /** + * Defines callback if route is not found + */ + public static function error($callback) + { + self::$error_callback = $callback; + } + + public static function haltOnMatch($flag = true) + { + self::$halts = $flag; + } + + /** + * Runs the callback for the given request + */ + public static function dispatch() + { + $uri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH); + $method = $_SERVER['REQUEST_METHOD']; + + $searches = array_keys(static::$patterns); + $replaces = array_values(static::$patterns); + + $found_route = false; + + self::$routes = str_replace('//', '/', self::$routes); + + // check if route is defined without regex + if (in_array($uri, self::$routes)) { + $route_pos = array_keys(self::$routes, $uri); + foreach ($route_pos as $route) { + + //using an ANY option to match both GET and POST requests + if (self::$methods[$route] == $method || self::$methods[$route] == 'ANY') { + $found_route = true; + + //if route is not an object + if(!is_object(self::$callbacks[$route])){ + + //grab all parts based on a / separator + $parts = explode('/',self::$callbacks[$route]); + + //collect the last index of the array + $last = end($parts); + + //grab the controller name and method call + $segments = explode('@',$last); + + //instanitate controller + $controller = new $segments[0](); + + //call method + $controller->$segments[1](); + + if (self::$halts) return; + + } else { + //call closure + call_user_func(self::$callbacks[$route]); + + if (self::$halts) return; + } + } + } + } else { + // check if defined with regex + $pos = 0; + foreach (self::$routes as $route) { + + if (strpos($route, ':') !== false) { + $route = str_replace($searches, $replaces, $route); + } + + if (preg_match('#^' . $route . '$#', $uri, $matched)) { + if (self::$methods[$pos] == $method) { + $found_route = true; + + array_shift($matched); //remove $matched[0] as [1] is the first parameter. + + + if(!is_object(self::$callbacks[$pos])){ + + //grab all parts based on a / separator + $parts = explode('/',self::$callbacks[$pos]); + + //collect the last index of the array + $last = end($parts); + + //grab the controller name and method call + $segments = explode('@',$last); + + //instanitate controller + $controller = new $segments[0](); + + //fix multi parameters + if(!method_exists($controller, $segments[1])){ + echo "controller and action not found"; + }else{ + call_user_func_array(array($controller, $segments[1]), $matched); + } + + //call method and pass any extra parameters to the method + // $controller->$segments[1](implode(",", $matched)); + + if (self::$halts) return; + } else { + call_user_func_array(self::$callbacks[$pos], $matched); + + if (self::$halts) return; + } + + } + } + $pos++; + } + } + + + // run the error callback if the route was not found + if ($found_route == false) { + if (!self::$error_callback) { + self::$error_callback = function() { + header($_SERVER['SERVER_PROTOCOL']." 404 Not Found"); + echo '404'; + }; + } + call_user_func(self::$error_callback); + } + } +} diff --git a/src/classes/RestApiManager.php b/src/classes/RestApiManager.php index 42aabe71..320a48e6 100644 --- a/src/classes/RestApiManager.php +++ b/src/classes/RestApiManager.php @@ -49,14 +49,14 @@ class RestApiManager{ $accessToken = $this->generateUserAccessToken($user)->getData(); if(!empty($accessTokenObj->id)){ $accessTokenObj->token = $accessToken; - $accessTokenObj->hash = base64_encode(CLIENT_BASE_URL).":".md5($accessTokenObj->token); + $accessTokenObj->hash = md5(CLIENT_BASE_URL.$accessTokenObj->token); $accessTokenObj->updated = date("Y-m-d H:i:s"); $accessTokenObj->Save(); }else{ $accessTokenObj = new RestAccessToken(); $accessTokenObj->userId = $user->id; $accessTokenObj->token = $accessToken; - $accessTokenObj->hash = base64_encode(CLIENT_BASE_URL).":".md5($accessTokenObj->token); + $accessTokenObj->hash = md5(CLIENT_BASE_URL.$accessTokenObj->token); $accessTokenObj->updated = date("Y-m-d H:i:s"); $accessTokenObj->created = date("Y-m-d H:i:s"); $accessTokenObj->Save(); @@ -69,13 +69,14 @@ class RestApiManager{ public function validateAccessToken($hash){ $accessTokenObj = new RestAccessToken(); + LogManager::getInstance()->info("AT Hash:".$hash); $accessTokenObj->Load("hash = ?",array($hash)); - + LogManager::getInstance()->info("AT Hash Object:".json_encode($accessTokenObj)); if(!empty($accessTokenObj->id) && $accessTokenObj->hash == $hash){ return $this->validateAccessTokenInner($accessTokenObj->token); } - return new IceResponse(IceResponse::ERROR, "Acess Token not found"); + return new IceResponse(IceResponse::ERROR, "Access Token not found"); } private function validateAccessTokenInner($accessToken){ @@ -122,30 +123,40 @@ class RestApiManager{ class RestEndPoint{ - var $url; - - public function setUrl($url){ - $this->url = $url; + + public function process($type , $parameter = NULL){ + $resp = $this->$type($parameter); + $this->printResponse($resp); } - public function getUrl(){ - return $this->url; + public function get($parameter){ + return new IceResponse(IceResponse::ERROR, "Method not Implemented"); } - public function get($parameters){ - return new IceResponse(IceResponse::ERROR, false); + public function post($parameter){ + return new IceResponse(IceResponse::ERROR, "Method not Implemented"); } - public function post($parameters){ - return new IceResponse(IceResponse::ERROR, false); + public function put($parameter){ + return new IceResponse(IceResponse::ERROR, "Method not Implemented"); } - public function put($parameters){ - return new IceResponse(IceResponse::ERROR, false); + public function delete($parameter){ + return new IceResponse(IceResponse::ERROR, "Method not Implemented"); } - - public function delete($parameters){ - return new IceResponse(IceResponse::ERROR, false); + + public function clearObject($obj){ + return BaseService::getInstance()->cleanUpAdoDB($obj); + } + + public function validateAccessToken(){ + $accessTokenValidation = RestApiManager::getInstance()->validateAccessToken($_REQUEST['access_token']); + + return $accessTokenValidation; + } + + public function printResponse($response){ + echo json_encode($response,JSON_PRETTY_PRINT); } } diff --git a/src/data.php b/src/data.php index a69c8d6b..01ef05de 100644 --- a/src/data.php +++ b/src/data.php @@ -90,7 +90,18 @@ if(in_array($table, BaseService::getInstance()->userTables) && !$skipProfileRest } $subordinatesIds.=$sub->id; } - $subordinatesIds.=""; + if($obj->allowIndirectMapping()){ + $indeirectEmployees = $subordinate->Find("indirect_supervisors IS NOT NULL and indirect_supervisors <> '' and status = 'Active'", array()); + foreach($indeirectEmployees as $ie){ + $indirectSupervisors = json_decode($ie->indirect_supervisors, true); + if(in_array($cemp, $indirectSupervisors)){ + if($subordinatesIds != ""){ + $subordinatesIds.=","; + } + $subordinatesIds.=$ie->id; + } + } + } $sql = "Select count(id) as count from ".$obj->_table." where ".SIGN_IN_ELEMENT_MAPPING_FIELD_NAME." in (".$subordinatesIds.") ".$countFilterQuery; LogManager::getInstance()->debug("Count Filter Query 2:".$sql); LogManager::getInstance()->debug("Count Filter Query Data 2:".json_encode($countFilterQueryData)); diff --git a/src/include.common.php b/src/include.common.php index ed7f75d1..ebf40b09 100644 --- a/src/include.common.php +++ b/src/include.common.php @@ -12,6 +12,7 @@ include (APP_BASE_PATH."utils/SessionUtils.php"); include (APP_BASE_PATH."utils/InputCleaner.php"); include (APP_BASE_PATH."utils/LogManager.php"); include (APP_BASE_PATH."utils/CalendarTools.php"); +include (APP_BASE_PATH."utils/EvalMath.php"); $_REQUEST = InputCleaner::cleanParameters($_REQUEST); diff --git a/src/js/mindmup-editabletable.js b/src/js/mindmup-editabletable.js new file mode 100644 index 00000000..6fa6c552 --- /dev/null +++ b/src/js/mindmup-editabletable.js @@ -0,0 +1,132 @@ +/*global $, window*/ +$.fn.editableTableWidget = function (options) { + 'use strict'; + return $(this).each(function () { + var buildDefaultOptions = function () { + var opts = $.extend({}, $.fn.editableTableWidget.defaultOptions); + opts.editor = opts.editor.clone(); + return opts; + }, + activeOptions = $.extend(buildDefaultOptions(), options), + ARROW_LEFT = 37, ARROW_UP = 38, ARROW_RIGHT = 39, ARROW_DOWN = 40, ENTER = 13, ESC = 27, TAB = 9, + element = $(this), + editor = activeOptions.editor.css('position', 'absolute').hide().appendTo(element.parent()), + active, + showEditor = function (select) { + //active = element.find('td:focus'); + active = element.find('td.editcell:focus'); + if (active.length) { + editor.val(active.text()) + .removeClass('error') + .show() + .offset(active.offset()) + .css(active.css(activeOptions.cloneProperties)) + .width(active.width()) + .height(active.height()) + .focus(); + if (select) { + editor.select(); + } + } + }, + setActiveText = function () { + var text = editor.val(), + evt = $.Event('change'), + originalContent; + if (active.text() === text || editor.hasClass('error')) { + return true; + } + originalContent = active.html(); + active.text(text).trigger(evt, text); + if (evt.result === false) { + active.html(originalContent); + } + }, + movement = function (element, keycode) { + if (keycode === ARROW_RIGHT) { + return element.next('td'); + } else if (keycode === ARROW_LEFT) { + return element.prev('td'); + } else if (keycode === ARROW_UP) { + return element.parent().prev().children().eq(element.index()); + } else if (keycode === ARROW_DOWN) { + return element.parent().next().children().eq(element.index()); + } + return []; + }; + editor.blur(function () { + setActiveText(); + editor.hide(); + }).keydown(function (e) { + if (e.which === ENTER) { + setActiveText(); + editor.hide(); + active.focus(); + e.preventDefault(); + e.stopPropagation(); + } else if (e.which === ESC) { + editor.val(active.text()); + e.preventDefault(); + e.stopPropagation(); + editor.hide(); + active.focus(); + } else if (e.which === TAB) { + active.focus(); + } else if (this.selectionEnd - this.selectionStart === this.value.length) { + var possibleMove = movement(active, e.which); + if (possibleMove.length > 0) { + possibleMove.focus(); + e.preventDefault(); + e.stopPropagation(); + } + } + }) + .on('input paste', function () { + var evt = $.Event('validate'); + active.trigger(evt, editor.val()); + if (evt.result === false) { + editor.addClass('error'); + } else { + editor.removeClass('error'); + } + }); + element.on('click keypress dblclick', showEditor) + .css('cursor', 'pointer') + .keydown(function (e) { + var prevent = true, + possibleMove = movement($(e.target), e.which); + if (possibleMove.length > 0) { + possibleMove.focus(); + } else if (e.which === ENTER) { + showEditor(false); + } else if (e.which === 17 || e.which === 91 || e.which === 93) { + showEditor(true); + prevent = false; + } else { + prevent = false; + } + if (prevent) { + e.stopPropagation(); + e.preventDefault(); + } + }); + + element.find('td').prop('tabindex', 1); + + $(window).on('resize', function () { + if (editor.is(':visible')) { + editor.offset(active.offset()) + .width(active.width()) + .height(active.height()); + } + }); + }); + +}; +$.fn.editableTableWidget.defaultOptions = { + cloneProperties: ['padding', 'padding-top', 'padding-bottom', 'padding-left', 'padding-right', + 'text-align', 'font', 'font-size', 'font-family', 'font-weight', + 'border', 'border-top', 'border-bottom', 'border-left', 'border-right'], + editor: $('') +}; + diff --git a/src/logout.php b/src/logout.php index 18e24404..935773e4 100644 --- a/src/logout.php +++ b/src/logout.php @@ -4,4 +4,4 @@ $_SESSION['user'] = null; session_destroy(); session_write_close(); $user = null; -header("Location:".CLIENT_BASE_URL."login.php"); \ No newline at end of file +header("Location:".CLIENT_BASE_URL."login.php?login=no"); \ No newline at end of file diff --git a/src/model/models.base.php b/src/model/models.base.php index bbebec99..e70ff0bb 100644 --- a/src/model/models.base.php +++ b/src/model/models.base.php @@ -61,6 +61,10 @@ class ICEHRM_Record extends ADOdb_Active_Record{ return $obj; } + public function postProcessGetElement($obj){ + return $obj; + } + public function getDefaultAccessLevel(){ return array("get","element","save","delete"); } diff --git a/src/model/models.inc.php b/src/model/models.inc.php index 01489c0b..4dceac0c 100644 --- a/src/model/models.inc.php +++ b/src/model/models.inc.php @@ -48,6 +48,17 @@ class Setting extends ICEHRM_Record { return array(); } + public function postProcessGetElement($obj){ + if($obj->name == 'Api: REST Api Token'){ + $user = BaseService::getInstance()->getCurrentUser(); + $dbUser = new User(); + $dbUser->Load("id = ?",array($user->id)); + $resp = RestApiManager::getInstance()->getAccessTokenForUser($dbUser); + $obj->value = $resp->getData(); + } + return $obj; + } + var $_table = 'Settings'; } diff --git a/src/modules.php b/src/modules.php index 5b7cc155..0eb6bd23 100644 --- a/src/modules.php +++ b/src/modules.php @@ -23,6 +23,12 @@ if(SettingsManager::getInstance()->getSetting("System: Add New Permissions") == SettingsManager::getInstance()->setSetting("System: Add New Permissions","0"); } +$resetModuleNames = false; +if(SettingsManager::getInstance()->getSetting("System: Reset Module Names") == "1"){ + $resetModuleNames = true; + SettingsManager::getInstance()->setSetting("System: Reset Module Names","0"); +} + function includeModuleManager($type,$name,$data){ $moduleCapsName = ucfirst($name); $moduleTypeCapsName = ucfirst($type); // Admin or Modules @@ -104,6 +110,12 @@ foreach($ams as $am){ createPermissions($meta, $dbModule->id); } + if($resetModuleNames){ + $dbModule->label = $arr['label']; + $dbModule->menu = $arr['menu']; + $dbModule->icon = $arr['icon']; + $dbModule->Save(); + } $arr['name'] = $dbModule->name; $arr['label'] = $dbModule->label; @@ -191,6 +203,12 @@ foreach($ams as $am){ createPermissions($meta, $dbModule->id); } + if($resetModuleNames){ + $dbModule->label = $arr['label']; + $dbModule->menu = $arr['menu']; + $dbModule->icon = $arr['icon']; + $dbModule->Save(); + } $arr['name'] = $dbModule->name; $arr['label'] = $dbModule->label; diff --git a/src/rest.php b/src/rest.php index 3a2e2923..1bb7a067 100644 --- a/src/rest.php +++ b/src/rest.php @@ -6,27 +6,26 @@ include ("config.base.php"); include ("include.common.php"); include("server.includes.inc.php"); -//clean request uri -LogManager::getInstance()->info("REQUEST_URI :".$_SERVER['REQUEST_URI']); -$parts = explode("?", $_SERVER['REQUEST_URI']); -$uri = $parts[0]; -if(substr($uri, -1) == "/"){ - $uri = substr($uri, 0, -1); -} +if(SettingsManager::getInstance()->getSetting('Api: REST Api Enabled') == '1') { -LogManager::getInstance()->info("REQUEST_URI Cleaned :".$uri); -$type = strtolower($_SERVER['REQUEST_METHOD']); + define('REST_API_PATH', '/api/'); + + LogManager::getInstance()->info("Request: " . $_REQUEST); + + \NoahBuscher\Macaw\Macaw::get(REST_API_PATH . 'echo', function () { + echo "Echo " . rand(); + }); + + $moduleManagers = BaseService::getInstance()->getModuleManagers(); + + foreach ($moduleManagers as $moduleManagerObj) { + + $moduleManagerObj->setupRestEndPoints(); + } + + \NoahBuscher\Macaw\Macaw::dispatch(); -$supportedMethods = array('get','post','put','delete'); -if(!in_array($type, $supportedMethods)){ - echo json_encode(new IceResponse(IceResponse::ERROR, "Method not supported")); - exit(); -} -$response = RestApiManager::getInstance()->process($type, $uri, $_REQUEST); -if($response->getStatus() == IceResponse::SUCCESS){ - echo json_encode($response,JSON_PRETTY_PRINT); }else{ - echo json_encode($response,JSON_PRETTY_PRINT); -} -exit(); \ No newline at end of file + echo "REST Api is not enabled. Please set 'Api: REST Api Enabled' setting to true"; +} \ No newline at end of file diff --git a/src/server.includes.inc.php b/src/server.includes.inc.php index 3811aef9..704c5e80 100644 --- a/src/server.includes.inc.php +++ b/src/server.includes.inc.php @@ -39,6 +39,7 @@ include (APP_BASE_PATH."classes/UIManager.php"); include (APP_BASE_PATH."classes/RestApiManager.php"); include (APP_BASE_PATH."classes/ModuleBuilder.php"); include (APP_BASE_PATH."classes/SimpleImage.php"); +include (APP_BASE_PATH."classes/Macaw.php"); include (APP_BASE_PATH."classes/crypt/Aes.php"); include (APP_BASE_PATH."classes/crypt/AesCtr.php"); @@ -112,7 +113,8 @@ if(defined('CLIENT_PATH')){ $moduleManagerObj->setupUserClasses($userTables); $moduleManagerObj->setupFileFieldMappings($fileFields); $moduleManagerObj->setupErrorMappings($mysqlErrors); - $moduleManagerObj->setupRestEndPoints(); + //$moduleManagerObj->setupRestEndPoints(); + $moduleManagerObj->initCalculationHooks(); $modelClassList = $moduleManagerObj->getModelClasses(); diff --git a/src/service.php b/src/service.php index 521071a0..fcd4f1ca 100644 --- a/src/service.php +++ b/src/service.php @@ -263,4 +263,34 @@ if($action == 'get'){ } -echo json_encode($ret); \ No newline at end of file +$res = json_encode($ret); + +if(empty($res) && !empty($ret)){ + //Do this only if there is a json encoding error + if(!empty($ret['object'])) { + if (is_array($ret['object'])) { + $newObjects = array(); + foreach ($ret['object'] as $obj) { + $newObjects[] = BaseService::getInstance()->cleanNonUTFChar($obj); + } + $ret['object'] = $newObjects; + } else { + $ret['object'] = BaseService::getInstance()->cleanNonUTFChar($ret['object']); + } + }else if(!empty($ret['data'])){ + if (is_array($ret['data'])) { + $newObjects = array(); + foreach ($ret['data'] as $obj) { + $newObjects[] = BaseService::getInstance()->cleanNonUTFChar($obj); + } + $ret['data'] = $newObjects; + } else { + $ret['data'] = BaseService::getInstance()->cleanNonUTFChar($ret['data']); + } + } + + + echo json_encode($ret); +}else{ + echo $res; +} diff --git a/src/utils/EvalMath.php b/src/utils/EvalMath.php new file mode 100644 index 00000000..7a666b65 --- /dev/null +++ b/src/utils/EvalMath.php @@ -0,0 +1,608 @@ + + +================================================================================ + +NAME + EvalMath - safely evaluate math expressions + +SYNOPSIS + evaluate('2+2'); + // supports: order of operation; parentheses; negation; built-in functions + $result = $m->evaluate('-8(5/2)^2*(1-sqrt(4))-8'); + // create your own variables + $m->evaluate('a = e^(ln(pi))'); + // or functions + $m->evaluate('f(x,y) = x^2 + y^2 - 2x*y + 1'); + // and then use them + $result = $m->evaluate('3*f(42,a)'); + ?> + +DESCRIPTION + Use the EvalMath class when you want to evaluate mathematical expressions + from untrusted sources. You can define your own variables and functions, + which are stored in the object. Try it, it's fun! + +METHODS + $m->evalute($expr) + Evaluates the expression and returns the result. If an error occurs, + prints a warning and returns false. If $expr is a function assignment, + returns true on success. + + $m->e($expr) + A synonym for $m->evaluate(). + + $m->vars() + Returns an associative array of all user-defined variables and values. + + $m->funcs() + Returns an array of all user-defined functions. + +PARAMETERS + $m->suppress_errors + Set to true to turn off warnings when evaluating expressions + + $m->last_error + If the last evaluation failed, contains a string describing the error. + (Useful when suppress_errors is on). + +AUTHOR INFORMATION + Copyright 2005, Miles Kaufmann. + +LICENSE + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are + met: + + 1 Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + 3. The name of the author may not be used to endorse or promote + products derived from this software without specific prior written + permission. + + THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR + IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, + INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN + ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + POSSIBILITY OF SUCH DAMAGE. + +*/ + +/** + * This class was heavily modified in order to get usefull spreadsheet emulation ;-) + * skodak + * + */ +if(!class_exists('EvalMath')) { + class EvalMath + { + + /** @var string Pattern used for a valid function or variable name. Note, var and func names are case insensitive. */ + private static $namepat = '[a-z][a-z0-9_]*'; + + var $suppress_errors = false; + var $last_error = null; + + var $v = array(); // variables (and constants) + var $f = array(); // user-defined functions + var $vb = array(); // constants + var $fb = array( // built-in functions + 'sin', 'sinh', 'arcsin', 'asin', 'arcsinh', 'asinh', + 'cos', 'cosh', 'arccos', 'acos', 'arccosh', 'acosh', + 'tan', 'tanh', 'arctan', 'atan', 'arctanh', 'atanh', + 'sqrt', 'abs', 'ln', 'log', 'exp', 'floor', 'ceil'); + + var $fc = array( // calc functions emulation + 'average' => array(-1), 'max' => array(-1), 'min' => array(-1), + 'mod' => array(2), 'pi' => array(0), 'power' => array(2), + 'round' => array(1, 2), 'sum' => array(-1), 'rand_int' => array(2), + 'rand_float' => array(0)); + + var $allowimplicitmultiplication; + + public function __construct($allowconstants = false, $allowimplicitmultiplication = false) + { + if ($allowconstants) { + $this->v['pi'] = pi(); + $this->v['e'] = exp(1); + } + $this->allowimplicitmultiplication = $allowimplicitmultiplication; + } + + /** + * Old syntax of class constructor. Deprecated in PHP7. + * + * @deprecated since Moodle 3.1 + */ + public function EvalMath($allowconstants = false, $allowimplicitmultiplication = false) + { + debugging('Use of class name as constructor is deprecated', DEBUG_DEVELOPER); + self::__construct($allowconstants, $allowimplicitmultiplication); + } + + function e($expr) + { + return $this->evaluate($expr); + } + + function evaluate($expr) + { + $this->last_error = null; + $expr = trim($expr); + if (substr($expr, -1, 1) == ';') $expr = substr($expr, 0, strlen($expr) - 1); // strip semicolons at the end + //=============== + // is it a variable assignment? + if (preg_match('/^\s*(' . self::$namepat . ')\s*=\s*(.+)$/', $expr, $matches)) { + if (in_array($matches[1], $this->vb)) { // make sure we're not assigning to a constant + return $this->trigger(get_string('cannotassigntoconstant', 'mathslib', $matches[1])); + } + if (($tmp = $this->pfx($this->nfx($matches[2]))) === false) return false; // get the result and make sure it's good + $this->v[$matches[1]] = $tmp; // if so, stick it in the variable array + return $this->v[$matches[1]]; // and return the resulting value + //=============== + // is it a function assignment? + } elseif (preg_match('/^\s*(' . self::$namepat . ')\s*\(\s*(' . self::$namepat . '(?:\s*,\s*' . self::$namepat . ')*)\s*\)\s*=\s*(.+)$/', $expr, $matches)) { + $fnn = $matches[1]; // get the function name + if (in_array($matches[1], $this->fb)) { // make sure it isn't built in + return $this->trigger(get_string('cannotredefinebuiltinfunction', 'mathslib', $matches[1])); + } + $args = explode(",", preg_replace("/\s+/", "", $matches[2])); // get the arguments + if (($stack = $this->nfx($matches[3])) === false) return false; // see if it can be converted to postfix + for ($i = 0; $i < count($stack); $i++) { // freeze the state of the non-argument variables + $token = $stack[$i]; + if (preg_match('/^' . self::$namepat . '$/', $token) and !in_array($token, $args)) { + if (array_key_exists($token, $this->v)) { + $stack[$i] = $this->v[$token]; + } else { + return $this->trigger(get_string('undefinedvariableinfunctiondefinition', 'mathslib', $token)); + } + } + } + $this->f[$fnn] = array('args' => $args, 'func' => $stack); + return true; + //=============== + } else { + return $this->pfx($this->nfx($expr)); // straight up evaluation, woo + } + } + + function vars() + { + return $this->v; + } + + function funcs() + { + $output = array(); + foreach ($this->f as $fnn => $dat) + $output[] = $fnn . '(' . implode(',', $dat['args']) . ')'; + return $output; + } + + /** + * @param string $name + * @return boolean Is this a valid var or function name? + */ + public static function is_valid_var_or_func_name($name) + { + return preg_match('/' . self::$namepat . '$/iA', $name); + } + + //===================== HERE BE INTERNAL METHODS ====================\\ + + // Convert infix to postfix notation + function nfx($expr) + { + + $index = 0; + $stack = new EvalMathStack; + $output = array(); // postfix form of expression, to be passed to pfx() + $expr = trim(strtolower($expr)); + + $ops = array('+', '-', '*', '/', '^', '_'); + $ops_r = array('+' => 0, '-' => 0, '*' => 0, '/' => 0, '^' => 1); // right-associative operator? + $ops_p = array('+' => 0, '-' => 0, '*' => 1, '/' => 1, '_' => 1, '^' => 2); // operator precedence + + $expecting_op = false; // we use this in syntax-checking the expression + // and determining when a - is a negation + + if (preg_match("/[^\w\s+*^\/()\.,-]/", $expr, $matches)) { // make sure the characters are all good + return $this->trigger(get_string('illegalcharactergeneral', 'mathslib', $matches[0])); + } + + while (1) { // 1 Infinite Loop ;) + $op = substr($expr, $index, 1); // get the first character at the current index + // find out if we're currently at the beginning of a number/variable/function/parenthesis/operand + $ex = preg_match('/^(' . self::$namepat . '\(?|\d+(?:\.\d*)?(?:(e[+-]?)\d*)?|\.\d+|\()/', substr($expr, $index), $match); + //=============== + if ($op == '-' and !$expecting_op) { // is it a negation instead of a minus? + $stack->push('_'); // put a negation on the stack + $index++; + } elseif ($op == '_') { // we have to explicitly deny this, because it's legal on the stack + return $this->trigger(get_string('illegalcharacterunderscore', 'mathslib')); // but not in the input expression + //=============== + } elseif ((in_array($op, $ops) or $ex) and $expecting_op) { // are we putting an operator on the stack? + if ($ex) { // are we expecting an operator but have a number/variable/function/opening parethesis? + if (!$this->allowimplicitmultiplication) { + return $this->trigger(get_string('implicitmultiplicationnotallowed', 'mathslib')); + } else {// it's an implicit multiplication + $op = '*'; + $index--; + } + } + // heart of the algorithm: + while ($stack->count > 0 and ($o2 = $stack->last()) and in_array($o2, $ops) and ($ops_r[$op] ? $ops_p[$op] < $ops_p[$o2] : $ops_p[$op] <= $ops_p[$o2])) { + $output[] = $stack->pop(); // pop stuff off the stack into the output + } + // many thanks: http://en.wikipedia.org/wiki/Reverse_Polish_notation#The_algorithm_in_detail + $stack->push($op); // finally put OUR operator onto the stack + $index++; + $expecting_op = false; + //=============== + } elseif ($op == ')' and $expecting_op) { // ready to close a parenthesis? + while (($o2 = $stack->pop()) != '(') { // pop off the stack back to the last ( + if (is_null($o2)) return $this->trigger(get_string('unexpectedclosingbracket', 'mathslib')); + else $output[] = $o2; + } + if (preg_match('/^(' . self::$namepat . ')\($/', $stack->last(2), $matches)) { // did we just close a function? + $fnn = $matches[1]; // get the function name + $arg_count = $stack->pop(); // see how many arguments there were (cleverly stored on the stack, thank you) + $fn = $stack->pop(); + $output[] = array('fn' => $fn, 'fnn' => $fnn, 'argcount' => $arg_count); // send function to output + if (in_array($fnn, $this->fb)) { // check the argument count + if ($arg_count > 1) { + $a = new stdClass(); + $a->expected = 1; + $a->given = $arg_count; + return $this->trigger(get_string('wrongnumberofarguments', 'mathslib', $a)); + } + } elseif (array_key_exists($fnn, $this->fc)) { + $counts = $this->fc[$fnn]; + if (in_array(-1, $counts) and $arg_count > 0) { + } elseif (!in_array($arg_count, $counts)) { + $a = new stdClass(); + $a->expected = implode('/', $this->fc[$fnn]); + $a->given = $arg_count; + return $this->trigger(get_string('wrongnumberofarguments', 'mathslib', $a)); + } + } elseif (array_key_exists($fnn, $this->f)) { + if ($arg_count != count($this->f[$fnn]['args'])) { + $a = new stdClass(); + $a->expected = count($this->f[$fnn]['args']); + $a->given = $arg_count; + return $this->trigger(get_string('wrongnumberofarguments', 'mathslib', $a)); + } + } else { // did we somehow push a non-function on the stack? this should never happen + return $this->trigger(get_string('internalerror', 'mathslib')); + } + } + $index++; + //=============== + } elseif ($op == ',' and $expecting_op) { // did we just finish a function argument? + while (($o2 = $stack->pop()) != '(') { + if (is_null($o2)) return $this->trigger(get_string('unexpectedcomma', 'mathslib')); // oops, never had a ( + else $output[] = $o2; // pop the argument expression stuff and push onto the output + } + // make sure there was a function + if (!preg_match('/^(' . self::$namepat . ')\($/', $stack->last(2), $matches)) + return $this->trigger(get_string('unexpectedcomma', 'mathslib')); + $stack->push($stack->pop() + 1); // increment the argument count + $stack->push('('); // put the ( back on, we'll need to pop back to it again + $index++; + $expecting_op = false; + //=============== + } elseif ($op == '(' and !$expecting_op) { + $stack->push('('); // that was easy + $index++; + $allow_neg = true; + //=============== + } elseif ($ex and !$expecting_op) { // do we now have a function/variable/number? + $expecting_op = true; + $val = $match[1]; + if (preg_match('/^(' . self::$namepat . ')\($/', $val, $matches)) { // may be func, or variable w/ implicit multiplication against parentheses... + if (in_array($matches[1], $this->fb) or array_key_exists($matches[1], $this->f) or array_key_exists($matches[1], $this->fc)) { // it's a func + $stack->push($val); + $stack->push(1); + $stack->push('('); + $expecting_op = false; + } else { // it's a var w/ implicit multiplication + $val = $matches[1]; + $output[] = $val; + } + } else { // it's a plain old var or num + $output[] = $val; + } + $index += strlen($val); + //=============== + } elseif ($op == ')') { + //it could be only custom function with no params or general error + if ($stack->last() != '(' or $stack->last(2) != 1) return $this->trigger(get_string('unexpectedclosingbracket', 'mathslib')); + if (preg_match('/^(' . self::$namepat . ')\($/', $stack->last(3), $matches)) { // did we just close a function? + $stack->pop();// ( + $stack->pop();// 1 + $fn = $stack->pop(); + $fnn = $matches[1]; // get the function name + $counts = $this->fc[$fnn]; + if (!in_array(0, $counts)) { + $a = new stdClass(); + $a->expected = $this->fc[$fnn]; + $a->given = 0; + return $this->trigger(get_string('wrongnumberofarguments', 'mathslib', $a)); + } + $output[] = array('fn' => $fn, 'fnn' => $fnn, 'argcount' => 0); // send function to output + $index++; + $expecting_op = true; + } else { + return $this->trigger(get_string('unexpectedclosingbracket', 'mathslib')); + } + //=============== + } elseif (in_array($op, $ops) and !$expecting_op) { // miscellaneous error checking + return $this->trigger(get_string('unexpectedoperator', 'mathslib', $op)); + } else { // I don't even want to know what you did to get here + return $this->trigger(get_string('anunexpectederroroccured', 'mathslib')); + } + if ($index == strlen($expr)) { + if (in_array($op, $ops)) { // did we end with an operator? bad. + return $this->trigger(get_string('operatorlacksoperand', 'mathslib', $op)); + } else { + break; + } + } + while (substr($expr, $index, 1) == ' ') { // step the index past whitespace (pretty much turns whitespace + $index++; // into implicit multiplication if no operator is there) + } + + } + while (!is_null($op = $stack->pop())) { // pop everything off the stack and push onto output + if ($op == '(') return $this->trigger(get_string('expectingaclosingbracket', 'mathslib')); // if there are (s on the stack, ()s were unbalanced + $output[] = $op; + } + return $output; + } + + // evaluate postfix notation + function pfx($tokens, $vars = array()) + { + + if ($tokens == false) return false; + + $stack = new EvalMathStack; + + foreach ($tokens as $token) { // nice and easy + + // if the token is a function, pop arguments off the stack, hand them to the function, and push the result back on + if (is_array($token)) { // it's a function! + $fnn = $token['fnn']; + $count = $token['argcount']; + if (in_array($fnn, $this->fb)) { // built-in function: + if (is_null($op1 = $stack->pop())) return $this->trigger(get_string('internalerror', 'mathslib')); + $fnn = preg_replace("/^arc/", "a", $fnn); // for the 'arc' trig synonyms + if ($fnn == 'ln') $fnn = 'log'; + eval('$stack->push(' . $fnn . '($op1));'); // perfectly safe eval() + } elseif (array_key_exists($fnn, $this->fc)) { // calc emulation function + // get args + $args = array(); + for ($i = $count - 1; $i >= 0; $i--) { + if (is_null($args[] = $stack->pop())) return $this->trigger(get_string('internalerror', 'mathslib')); + } + $res = call_user_func_array(array('EvalMathFuncs', $fnn), array_reverse($args)); + if ($res === FALSE) { + return $this->trigger(get_string('internalerror', 'mathslib')); + } + $stack->push($res); + } elseif (array_key_exists($fnn, $this->f)) { // user function + // get args + $args = array(); + for ($i = count($this->f[$fnn]['args']) - 1; $i >= 0; $i--) { + if (is_null($args[$this->f[$fnn]['args'][$i]] = $stack->pop())) return $this->trigger(get_string('internalerror', 'mathslib')); + } + $stack->push($this->pfx($this->f[$fnn]['func'], $args)); // yay... recursion!!!! + } + // if the token is a binary operator, pop two values off the stack, do the operation, and push the result back on + } elseif (in_array($token, array('+', '-', '*', '/', '^'), true)) { + if (is_null($op2 = $stack->pop())) return $this->trigger(get_string('internalerror', 'mathslib')); + if (is_null($op1 = $stack->pop())) return $this->trigger(get_string('internalerror', 'mathslib')); + switch ($token) { + case '+': + $stack->push($op1 + $op2); + break; + case '-': + $stack->push($op1 - $op2); + break; + case '*': + $stack->push($op1 * $op2); + break; + case '/': + if ($op2 == 0) return $this->trigger(get_string('divisionbyzero', 'mathslib')); + $stack->push($op1 / $op2); + break; + case '^': + $stack->push(pow($op1, $op2)); + break; + } + // if the token is a unary operator, pop one value off the stack, do the operation, and push it back on + } elseif ($token == "_") { + $stack->push(-1 * $stack->pop()); + // if the token is a number or variable, push it on the stack + } else { + if (is_numeric($token)) { + $stack->push($token); + } elseif (array_key_exists($token, $this->v)) { + $stack->push($this->v[$token]); + } elseif (array_key_exists($token, $vars)) { + $stack->push($vars[$token]); + } else { + return $this->trigger(get_string('undefinedvariable', 'mathslib', $token)); + } + } + } + // when we're out of tokens, the stack should have a single element, the final result + if ($stack->count != 1) return $this->trigger(get_string('internalerror', 'mathslib')); + return $stack->pop(); + } + + // trigger an error, but nicely, if need be + function trigger($msg) + { + $this->last_error = $msg; + if (!$this->suppress_errors) trigger_error($msg, E_USER_WARNING); + return false; + } + + } + +// for internal use + class EvalMathStack + { + + var $stack = array(); + var $count = 0; + + function push($val) + { + $this->stack[$this->count] = $val; + $this->count++; + } + + function pop() + { + if ($this->count > 0) { + $this->count--; + return $this->stack[$this->count]; + } + return null; + } + + function last($n = 1) + { + if ($this->count - $n >= 0) { + return $this->stack[$this->count - $n]; + } + return null; + } + } + + +// spreadsheet functions emulation + class EvalMathFuncs + { + + static function average() + { + $args = func_get_args(); + return (call_user_func_array(array('self', 'sum'), $args) / count($args)); + } + + static function max() + { + $args = func_get_args(); + $res = array_pop($args); + foreach ($args as $a) { + if ($res < $a) { + $res = $a; + } + } + return $res; + } + + static function min() + { + $args = func_get_args(); + $res = array_pop($args); + foreach ($args as $a) { + if ($res > $a) { + $res = $a; + } + } + return $res; + } + + static function mod($op1, $op2) + { + return $op1 % $op2; + } + + static function pi() + { + return pi(); + } + + static function power($op1, $op2) + { + return pow($op1, $op2); + } + + static function round($val, $precision = 0) + { + return round($val, $precision); + } + + static function sum() + { + $args = func_get_args(); + $res = 0; + foreach ($args as $a) { + $res += $a; + } + return $res; + } + + protected static $randomseed = null; + + static function set_random_seed($randomseed) + { + self::$randomseed = $randomseed; + } + + static function get_random_seed() + { + if (is_null(self::$randomseed)) { + return microtime(); + } else { + return self::$randomseed; + } + } + + static function rand_int($min, $max) + { + if ($min >= $max) { + return false; //error + } + $noofchars = ceil(log($max + 1 - $min, '16')); + $md5string = md5(self::get_random_seed()); + $stringoffset = 0; + do { + while (($stringoffset + $noofchars) > strlen($md5string)) { + $md5string .= md5($md5string); + } + $randomno = hexdec(substr($md5string, $stringoffset, $noofchars)); + $stringoffset += $noofchars; + } while (($min + $randomno) > $max); + return $min + $randomno; + } + + static function rand_float() + { + $randomvalues = unpack('v', md5(self::get_random_seed(), true)); + return array_shift($randomvalues) / 65536; + } + } +} \ No newline at end of file