diff --git a/README.md b/README.md index 4bf0acc84..bf2d5f80b 100644 --- a/README.md +++ b/README.md @@ -137,7 +137,7 @@ or clone with Git : git clone https://github.com/equalframework/equal.git ``` -For more info, see : [http://doc.equal.run/getting-started/installation](http://doc.equal.run/getting-started/installation/) +For more info, see : [https://doc.equal.run/getting-started/install/installation/](https://doc.equal.run/getting-started/install/installation/) ## Contributing Contributions are what make the open-source community such an amazing place to learn, inspire, and create. diff --git a/config/schema.json b/config/schema.json index 475328936..00f84d875 100644 --- a/config/schema.json +++ b/config/schema.json @@ -316,6 +316,12 @@ "description": "Flag for requesting to store meta data whenever an event occurs (creation, update, deletion or custom event).", "help": "Keep in mind that enabling logging increases I/O operations and impacts performances." }, + "LOGS_EXPIRY_DELAY": { + "type": "integer", + "default": 12, + "description": "Duration, in months, for retaining logs in the database.", + "help": "This value is use in the logs_prune controller for auto-vacuuming logs." + }, "UPLOAD_MAX_FILE_SIZE": { "type": "integer", "usage": "amount/data", diff --git a/eq.lib.php b/eq.lib.php index 5df21d78d..de0c070a7 100644 --- a/eq.lib.php +++ b/eq.lib.php @@ -1035,7 +1035,24 @@ public static function announce(array $announcement) { foreach($announcement['params'] as $param => $config) { // #memo - at some point condition had a clause "|| empty($body[$param])", remember not to alter received data! if(in_array($param, $missing_params) && isset($config['default'])) { - $body[$param] = $config['default']; + $default_value = $config['default']; + // #memo - array can be used as callable descriptor but are not considered here + if( (is_string($default_value) || is_object($default_value)) && is_callable($default_value)) { + // either a php function (or a function from the global scope) or a closure object + if(is_object($default_value)) { + // default is a closure + $default_value = $default_value(); + } + } + elseif(is_string($default_value) && strpos($default_value, '::')) { + list($class_name, $method_name) = explode('::', $default_value); + if(method_exists($class_name, $method_name)) { + /** @var \equal\orm\ObjectManager */ + $orm = $container->get('orm'); + $default_value = $orm->callonce($class_name, $method_name); + } + } + $body[$param] = $default_value; } if(!array_key_exists($param, $body)) { // ignore optional params without default value (this allows PATCH of objects on specific fields only) diff --git a/lib/equal/data/adapt/adapters/sql/DataAdapterSqlBoolean.class.php b/lib/equal/data/adapt/adapters/sql/DataAdapterSqlBoolean.class.php index 54cf694fb..40dbd5a5d 100644 --- a/lib/equal/data/adapt/adapters/sql/DataAdapterSqlBoolean.class.php +++ b/lib/equal/data/adapt/adapters/sql/DataAdapterSqlBoolean.class.php @@ -52,7 +52,7 @@ public function adaptOut($value, $usage, $locale='en') { if(is_null($value)) { return null; } - return ($value)?'1':'0'; + return ($value) ? '1': '0'; } } diff --git a/lib/equal/error/Reporter.class.php b/lib/equal/error/Reporter.class.php index 770228e14..fc5d8d028 100644 --- a/lib/equal/error/Reporter.class.php +++ b/lib/equal/error/Reporter.class.php @@ -55,6 +55,7 @@ public static function handleThrowable($exception) { $msg = $exception->getMessage(); // retrieve instance and log error $instance = self::getInstance(); + // #todo #bug - $backtrace may contain non json_encodable objects (which leads to an error at file_put_contents) $backtrace = $exception->getTrace(); if(count($backtrace)) { $trace = array_shift($backtrace); @@ -159,12 +160,13 @@ private function log($code, $msg, $trace) { 'mtime' => substr($time_parts[0], 2, 6), 'level' => qn_debug_code_name($code), 'mode' => qn_debug_mode_name($mode), - 'class' => (isset($trace['class']))?$trace['class']:'', - 'function' => (isset($trace['function']))?(strlen($trace['function'])?$trace['function'].'()':'[main]'):'', - 'file' => (isset($trace['file']))?$trace['file']:'', - 'line' => (isset($trace['line']))?$trace['line']:'', + 'class' => (isset($trace['class'])) ? $trace['class'] : '', + 'function' => (isset($trace['function'])) ? (strlen($trace['function'])?$trace['function'].'()':'[main]') : '', + 'file' => (isset($trace['file'])) ? $trace['file'] : '', + 'line' => (isset($trace['line'])) ? $trace['line'] : '', 'message' => $msg, - 'stack' => (isset($trace['stack']))?$trace['stack']:[] + // #memo - in case of error, forcing trace stack to empty array + 'stack' => (isset($trace['stack'])) ? $trace['stack'] : [] ]; // append backtrace if required (fatal errors) diff --git a/lib/equal/orm/Collection.class.php b/lib/equal/orm/Collection.class.php index 441a0deb3..97964c481 100644 --- a/lib/equal/orm/Collection.class.php +++ b/lib/equal/orm/Collection.class.php @@ -835,14 +835,14 @@ public function read($fields, $lang=null) { } $children_fields = []; foreach($subfields as $key => $val) { - $children_fields[] = (!is_numeric($key))?$key:$val; + $children_fields[] = (!is_numeric($key)) ? $key : $val; } // read all targeted children objects at once - $this->orm->read($target['foreign_object'], $children_ids, $children_fields, ($lang)?$lang:$this->lang); + $this->orm->read($target['foreign_object'], $children_ids, $children_fields, ($lang) ? $lang : $this->lang); // assign retrieved values to the objects they relate to foreach($this->objects as $id => $object) { /** @var Collection */ - $children = $target['foreign_object']::ids($this->objects[$id][$field])->read($subfields, ($lang)?$lang:$this->lang); + $children = $target['foreign_object']::ids($this->objects[$id][$field])->read($subfields, ($lang) ? $lang : $this->lang); if($target['result_type'] == 'many2one') { // #memo - result might be either null or a Model object (which might contain sub-collections) $this->objects[$id][$field] = $children->first(); @@ -973,7 +973,11 @@ public function delete($permanent=false) { public function transition($transition) { // retrieve targeted identifiers $res = $this->orm->transition($this->class, $this->ids(), $transition); - if(count($res)) { + if($res < 0) { + trigger_error("ORM::unexpected error for transition '{$transition}' on '{$this->class}' objects:".$this->orm->getLastError(), EQ_REPORT_WARNING); + throw new \Exception('transition_failed', $res); + } + elseif(count($res)) { throw new \Exception(serialize($res), EQ_ERROR_NOT_ALLOWED); } return $this; diff --git a/lib/equal/orm/DateReference.class.php b/lib/equal/orm/DateReference.class.php index 91852cd33..19591f041 100644 --- a/lib/equal/orm/DateReference.class.php +++ b/lib/equal/orm/DateReference.class.php @@ -43,7 +43,7 @@ public function parse($descriptor) { if(preg_match('/date\.(this|prev|next)(\((\d*)\))?\.(day|week|month|quarter|semester|year)(\.(first|last|get\((.+)\)))?/', $descriptor, $matches)) { // init at today - $date = new DateTime(); + $date = new \DateTime(); $origin = $matches[1]; $offset = isset($matches[3]) && $matches[3] !== '' ? (int)$matches[3] : 1; diff --git a/lib/equal/orm/Model.class.php b/lib/equal/orm/Model.class.php index fddeed5ce..ae3fa11f3 100644 --- a/lib/equal/orm/Model.class.php +++ b/lib/equal/orm/Model.class.php @@ -6,6 +6,7 @@ */ namespace equal\orm; +use core\setting\Setting; use equal\services\Container; /** @@ -141,6 +142,7 @@ private function setDefaults($values=[]) { $container = Container::getInstance(); $orm = $container->get('orm'); $defaults = $this->getDefaults(); + $setting_defaults = $this->getSettingDefaults(); // reset fields values $this->values = []; $fields = array_keys($this->schema); @@ -166,6 +168,32 @@ private function setDefaults($values=[]) { // default is a method of the class (or parents') $this->values[$field] = $orm->callonce($this->getType(), $defaults[$field]); } + elseif($defaults[$field] === 'defaultFromSetting') { + $class_name = get_called_class(); + + // create the setting code prefix + // @example "core\alert\MessageModel" --> alert.message_model + + // split parts into an array + $parts = explode('\\', $class_name); + $package = array_shift($parts); + + // use dots instead of backslashes + $class_name = implode('.', $parts); + // convert PascalCase to snake_case + $setting_code_prefix = strtolower(preg_replace('/(?values[$field] = $default; + } + } else { // default is a scalar value $this->values[$field] = $defaults[$field]; @@ -513,6 +541,16 @@ public function getDefaults() { return $defaults; } + public function getSettingDefaults() { + $setting_defaults = []; + foreach($this->schema as $field => $definition) { + if(isset($definition['setting_default'])) { + $setting_defaults[$field] = $definition['setting_default']; + } + } + return $setting_defaults; + } + /** * Provide the list of unique rules (array of combinations of fields). * This method can be overridden to define a more precise set of unique constraints (i.e when keys are formed of several fields). diff --git a/lib/equal/orm/ObjectManager.class.php b/lib/equal/orm/ObjectManager.class.php index a75358f43..9ab84de66 100644 --- a/lib/equal/orm/ObjectManager.class.php +++ b/lib/equal/orm/ObjectManager.class.php @@ -644,8 +644,9 @@ private function load($class, $ids, $fields, $lang) { if(!ObjectManager::checkFieldAttributes(self::$mandatory_attributes, $schema, $field)) { throw new Exception("missing at least one mandatory attribute for field '$field' of class '$class'", QN_ERROR_INVALID_PARAM); } - $order = (isset($schema[$field]['order']) && isset($schema[$schema[$field]['order']]))?$schema[$field]['order']:'id'; - $sort = (isset($schema[$field]['sort']))?$schema[$field]['sort']:'asc'; + // #todo - we should check that order field exists in targeted entity + $order = (isset($schema[$field]['order'])) ? $schema[$field]['order'] : 'id'; + $sort = (isset($schema[$field]['sort'])) ? $schema[$field]['sort'] : 'asc'; $domain = [ [ [$schema[$field]['foreign_field'], 'in', $ids], @@ -661,9 +662,12 @@ private function load($class, $ids, $fields, $lang) { $domain = $domain_tmp->toArray(); } // #todo - add support for sorting on m2o fields (for now user needs to use usort) + // #todo - this is invalid, check should point to the target schema (foreign_object) + /* if($schema[$order]['type'] == 'many2one' || (isset($schema[$order]['result_type']) && $schema[$order]['result_type'] == 'many2one') ) { $order = 'id'; } + */ // obtain the ids by searching inside the foreign object's table $result = $om->db->getRecords( $om->getObjectTableName($schema[$field]['foreign_object']), @@ -844,6 +848,7 @@ private function load($class, $ids, $fields, $lang) { } catch(Exception $e) { trigger_error("ORM::".$e->getMessage(), QN_REPORT_ERROR); + $this->last_error = $e->getMessage(); throw new Exception('unable to load object fields', $e->getCode()); } } @@ -1104,6 +1109,7 @@ private function store($class, $ids, $fields, $lang) { } catch (Exception $e) { trigger_error("ORM::".$e->getMessage(), QN_REPORT_ERROR); + $this->last_error = $e->getMessage(); throw new Exception('unable to store object fields', $e->getCode()); } } @@ -1129,7 +1135,7 @@ public function callonce($class, $method, $ids=[], $values=[], $lang=null, $sign $result = []; - $lang = ($lang)?$lang:constant('DEFAULT_LANG'); + $lang = ($lang) ? $lang : constant('DEFAULT_LANG'); $called_class = $class; $called_method = $method; @@ -1212,14 +1218,16 @@ public function callonce($class, $method, $ids=[], $values=[], $lang=null, $sign if($res !== null) { $result = $res; } + // unstack global object_methods state + $this->object_methods = $object_methods_state; } catch(\Exception $e) { - $result = $e->getCode(); + // #memo - there is no depth limit so, exceptions must be relayed to caller + // unstack global object_methods state + $this->object_methods = $object_methods_state; + throw $e; } - // unstack global object_methods state - $this->object_methods = $object_methods_state; - return $result; } @@ -2504,7 +2512,7 @@ public function canTransition($class, $ids, $transition) { * @param array $ids Array of ids of the objects to delete. * @param string $transition Name of the requested workflow transition (signal). * - * @return array Returns an associative array containing invalid fields with their associated error_message_id. + * @return mixed Returns an associative array containing invalid fields with their associated error_message_id. * An empty array means all fields are valid. In case of error, the method returns a negative integer. */ public function transition($class, $ids, $transition) { @@ -2514,28 +2522,36 @@ public function transition($class, $ids, $transition) { if(count($res)) { return $res; } + $res = []; $table_name = $this->getObjectTableName($class); $model = $this->getStaticInstance($class); $workflow = $model->getWorkflow(); $lang = constant('DEFAULT_LANG'); // read status field for retrieved objects $objects = $this->read($class, $ids, ['status']); - foreach($objects as $id => $object) { - // reaching this part means all objects have a status and a workflow in which given transition is defined and valid for requested mutation - $t_descr = $workflow[$object['status']]['transitions'][$transition]; - // if a 'onbefore' method is defined for applied transition, call it - if(isset($t_descr['onbefore'])) { - $this->callonce($class, $t_descr['onbefore'], $id); - } - // status field is always writeable (we don't call `update()` to bypass checks) - $this->cache[$table_name][$id][$lang]['status'] = $t_descr['status']; - $this->store($class, (array) $id, ['status'], $lang); - // if a 'onafter' method is defined for applied transition, call it - if(isset($t_descr['onafter'])) { - $this->callonce($class, $t_descr['onafter'], $id); + try { + foreach($objects as $id => $object) { + // reaching this part means all objects have a status and a workflow in which given transition is defined and valid for requested mutation + $t_descr = $workflow[$object['status']]['transitions'][$transition]; + // if a 'onbefore' method is defined for applied transition, call it + if(isset($t_descr['onbefore'])) { + $this->callonce($class, $t_descr['onbefore'], $id); + } + // status field is always writeable (we don't call `update()` to bypass checks) + $this->cache[$table_name][$id][$lang]['status'] = $t_descr['status']; + $this->store($class, (array) $id, ['status'], $lang); + // if a 'onafter' method is defined for applied transition, call it + if(isset($t_descr['onafter'])) { + $this->callonce($class, $t_descr['onafter'], $id); + } } } - return []; + catch(\Exception $e) { + trigger_error("ORM::".$e->getMessage(), QN_REPORT_WARNING); + $this->last_error = $e->getMessage(); + $res = $e->getCode(); + } + return $res; } /** diff --git a/packages/core/actions/config/generate.php b/packages/core/actions/config/generate.php index 08c3df3cd..c5e90d9a6 100644 --- a/packages/core/actions/config/generate.php +++ b/packages/core/actions/config/generate.php @@ -5,91 +5,103 @@ Licensed under GNU LGPL 3 license */ -list( $params, $providers ) = eQual::announce( [ - 'description' => "Generate a configuration file based on a set of params and store it as `config/config.json`.", - 'response' => [ - 'content-type' => 'application/json', - 'charset' => 'UTF-8', - 'accept-origin' => '*' - ], - 'params' => [ +[$params, $providers] = eQual::announce([ + 'description' => "Generate a configuration file based on a set of params and store it as `config/config.json`.", + 'params' => [ 'domain_name' => [ - 'description' => 'The domain name of the installation (virtual host).', - 'type' => 'string', - 'default' => getenv('VIRTUAL_HOST') + 'description' => "The domain name of the installation (virtual host).", + 'type' => 'string', + 'default' => getenv('VIRTUAL_HOST') + ], + 'scheme' => [ + 'description' => "The scheme of the installation (https method).", + 'type' => 'string', + 'selection' => [ + 'http', + 'https' + ], + 'default' => (getenv('HTTPS_METHOD') === 'noredirect') ? 'http' : 'https' ], 'dbms' => [ - 'description' => 'DMBS software brand.', - 'type' => 'string', - 'selection' => [ + 'description' => "DBMS software brand.", + 'type' => 'string', + 'selection' => [ 'MYSQL', 'SQLSRV', 'MARIADB', 'SQLITE', 'POSTGRESQL' ], - 'required' => true + 'required' => true ], 'db_host' => [ - 'description' => 'The host of the database.', - 'type' => 'string', - 'required' => true + 'description' => "The host of the database.", + 'type' => 'string', + 'required' => true ], 'db_port' => [ - 'description' => 'The tcp port of the DBMS host.', - 'type' => 'integer', - 'required' => true + 'description' => "The tcp port of the DBMS host.", + 'type' => 'integer', + 'required' => true ], 'db_name' => [ - 'description' => 'The table name of the database.', - 'type' => 'string', - 'required' => true + 'description' => "The table name of the database.", + 'type' => 'string', + 'required' => true ], 'db_username' => [ - 'description' => 'The username of the DBMS host.', - 'type' => 'string', - 'required' => true + 'description' => "The username of the DBMS host.", + 'type' => 'string', + 'required' => true ], 'db_password' => [ - 'description' => 'The password of the DBMS host.', - 'type' => 'string', - 'required' => true + 'description' => "The password of the DBMS host.", + 'type' => 'string', + 'required' => true ] ], - 'providers' => [ 'context' ] -] ); + 'response' => [ + 'content-type' => 'application/json', + 'charset' => 'UTF-8', + 'accept-origin' => '*' + ], + 'providers' => ['context'] +]); /** * @var \equal\php\Context $context */ -list( $context ) = [ $providers['context'] ]; - -$domain_name = $params['domain_name'] ?? 'localhost'; -$scheme = (getenv('HTTPS_METHOD') == 'noredirect') ? 'http' : 'https'; +['context' => $context] = $providers; $config = [ - "DB_DBMS" => $params['dbms'], - "DB_HOST" => $params['db_host'], - "DB_PORT" => $params['db_port'], - "DB_USER" => $params['db_username'], - "DB_PASSWORD" => $params['db_password'], - "DB_NAME" => $params['db_name'], - "AUTH_SECRET_KEY" => bin2hex( random_bytes( 32 ) ), - "AUTH_ACCESS_TOKEN_VALIDITY" => "1d", - "USER_ACCOUNT_DISPLAYNAME" => "nickname", - "BACKEND_URL" => $scheme.'://'.$domain_name, - "REST_API_URL" => $scheme.'://'.$domain_name.'/' + 'DB_DBMS' => $params['dbms'], + 'DB_HOST' => $params['db_host'], + 'DB_PORT' => $params['db_port'], + 'DB_USER' => $params['db_username'], + 'DB_PASSWORD' => $params['db_password'], + 'DB_NAME' => $params['db_name'], + 'AUTH_SECRET_KEY' => bin2hex(random_bytes(32)), + 'AUTH_ACCESS_TOKEN_VALIDITY' => "1d", + 'USER_ACCOUNT_DISPLAYNAME' => "nickname", + 'BACKEND_URL' => $params['scheme'].'://'.$params['domain_name'], + 'REST_API_URL' => $params['scheme'].'://'.$params['domain_name'].'/' ]; $filepath = EQ_BASEDIR.'/config/config.json'; -// make sure file is writable -if( !is_writable(dirname($filepath)) || !is_writable($filepath) ) { - throw new Exception( 'non_writable_config', EQ_ERROR_INVALID_CONFIG ); + +if(!is_writable(dirname($filepath))) { + throw new Exception("non_writable_config", EQ_ERROR_INVALID_CONFIG); } -if( !file_exists($filepath) ) { - throw new Exception( 'config_already_exists', EQ_ERROR_NOT_ALLOWED ); + +if(file_exists($filepath)) { + throw new Exception("config_already_exists", EQ_ERROR_NOT_ALLOWED); } -// store config -file_put_contents($filepath, json_encode($config, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT)); -// HTTP 201 Created -$context->httpResponse()->status(201); + +file_put_contents( + $filepath, + json_encode($config, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT) +); + +$context->httpResponse() + ->status(201) + ->send(); diff --git a/packages/core/actions/init/package.php b/packages/core/actions/init/package.php index 09a4d64aa..f2d5e44ee 100644 --- a/packages/core/actions/init/package.php +++ b/packages/core/actions/init/package.php @@ -31,6 +31,11 @@ 'type' => 'boolean', 'default' => false ], + 'force_cascade' => [ + 'description' => 'Force initialization for dependencies as well.', + 'type' => 'boolean', + 'default' => false + ], 'import' => [ 'description' => 'Request importing initial data.', 'type' => 'boolean', @@ -173,6 +178,7 @@ 'package' => $dependency, 'cascade' => $params['cascade'], 'import' => $params['import'] && $params['import_cascade'], + 'force' => $params['force'] && $params['force_cascade'], 'root' => false ], true); diff --git a/packages/core/actions/logs/prune.php b/packages/core/actions/logs/prune.php new file mode 100644 index 000000000..fc5c5ca4f --- /dev/null +++ b/packages/core/actions/logs/prune.php @@ -0,0 +1,62 @@ + + Some Rights Reserved, The eQual Framework, 2010-2024 + Author: The eQual Framework Contributors + Original Author: Cedric Francoys + Licensed under GNU LGPL 3 license +*/ +use core\Log; +use core\setting\Setting; + +list($params, $providers) = eQual::announce([ + 'description' => 'Prunes log items based on the retention duration defined in the auto-vacuum setting.', + 'params' => [], + 'response' => [ + 'content-type' => 'application/json', + 'charset' => 'UTF-8', + 'accept-origin' => '*' + ], + 'access' => [ + 'visibility' => 'protected', + 'groups' => ['admins'] + ], + 'constants' => ['LOGS_EXPIRY_DELAY'], + 'providers' => ['context'] +]); + +/** + * @var \equal\php\Context $context + */ +['context' => $context] = $providers; + + +// retrieve logs expiry delay, in months +$delay = Setting::get('core', 'main', 'logs.expiry', constant('LOGS_EXPIRY_DELAY')); + +// compute pivot date for removing older logs +$time = strtotime("-$delay months"); + +if($time >= time() || $time <= 0) { + throw new Exception('unexpected_error', EQ_ERROR_UNKNOWN); +} + +$requests_threshold = 100; +$requests_count = 0; + +$collection = Log::search(['created', '<=', $time], ['limit' => 1000]); + +while(count($collection->ids())) { + $collection->delete(true); + $collection = Log::search(['created', '<=', $time], ['limit' => 1000]); + ++$requests_count; + + if($requests_count >= $requests_threshold) { + $requests_count = 0; + sleep(5); + } +} + +$context->httpResponse() + ->status(204) + ->send(); diff --git a/packages/core/actions/utils/sqldesigner/update.php b/packages/core/actions/utils/sqldesigner/update.php deleted file mode 100644 index abb73c9f8..000000000 --- a/packages/core/actions/utils/sqldesigner/update.php +++ /dev/null @@ -1,46 +0,0 @@ - - Some Rights Reserved, Cedric Francoys, 2010-2021 - Licensed under GNU GPL 3 license -*/ -list($params, $providers) = announce([ - 'description' => "Returns the schema of given class (model)", - 'params' => [ - 'package' => [ - 'description' => 'Name of the package for which the schema is requested', - 'type' => 'string', - 'required' => true - ], - 'xml' => [ - 'description' => 'Updated XML of the schema for given package', - 'type' => 'string', - 'required' => true - ], - ], - 'response' => [ - 'content-type' => 'application/json', - 'charset' => 'utf-8', - 'accept-origin' => '*' - ], - 'providers' => ['context', 'auth'] -]); - - -list($context, $auth) = [ $providers['context'], $providers['auth'] ]; - -// retrieve related cache-id -$cache_id = md5($auth->userId().'::'.'get'.'::'.'utils_sqldesigner_schema'); - -// update cached response, if any -$cache_filename = QN_BASEDIR.'/cache/'.$cache_id; -if(file_exists($cache_filename)) { - // retrieve headers - list($headers, $result) = unserialize(file_get_contents($cache_filename)); - file_put_contents($cache_filename, serialize([$headers, $params['xml']])); -} - -$context->httpResponse() - ->status(204) - ->body('') - ->send(); \ No newline at end of file diff --git a/packages/core/apps/app/version b/packages/core/apps/app/version index 9ff651dfd..feec9e769 100644 --- a/packages/core/apps/app/version +++ b/packages/core/apps/app/version @@ -1 +1 @@ -b3078e1544e620e9da4b6632a1eafab5 +753600ef0b8de42432eb5c3429879a74 diff --git a/packages/core/apps/app/web.app b/packages/core/apps/app/web.app index cfffd4078..7b9d60db4 100644 Binary files a/packages/core/apps/app/web.app and b/packages/core/apps/app/web.app differ diff --git a/packages/core/apps/apps/version b/packages/core/apps/apps/version index b55c7ced7..d65052596 100644 --- a/packages/core/apps/apps/version +++ b/packages/core/apps/apps/version @@ -1 +1 @@ -67985d85f939b2734c5510eabc7d253c +7ef695ed786fd5c6984faa15cb2de4cc diff --git a/packages/core/apps/apps/web.app b/packages/core/apps/apps/web.app index e0873d89d..e6e2ed962 100644 Binary files a/packages/core/apps/apps/web.app and b/packages/core/apps/apps/web.app differ diff --git a/packages/core/classes/setting/Setting.class.php b/packages/core/classes/setting/Setting.class.php index 61b81fdfe..9a07f18b5 100644 --- a/packages/core/classes/setting/Setting.class.php +++ b/packages/core/classes/setting/Setting.class.php @@ -96,13 +96,20 @@ public static function getColumns() { 'boolean', 'integer', 'float', - 'string' + 'string', + 'many2one' ], 'description' => 'The format of data stored by the param.', 'default' => 'string', 'visible' => ['is_sequence', '=', false] ], + 'object_class' => [ + 'type' => 'string', + 'description' => "Full name of the entity the Setting refers to.", + 'visible' => ['type', '=', 'many2one'] + ], + 'is_multilang' => [ 'type' => 'boolean', 'description' => "Marks the setting as translatable.", @@ -181,16 +188,14 @@ public function getUnique() { /** * Retrieve the value of a given setting. - * - * @param string $package Package to which the setting relates to. - * @param string $section Specific section within the package. - * @param string $code Unique code of the setting within the given package and section. - * @param mixed $default (optional) Default value to return if setting is not found. - * @param array $selector (optional) Map used as filter to target a specific value (ex. `[user_id => 2]`). - * @param string|null $lang (optional) Lang in which to retrieve the value (for multilang settings). + * This is a shorthand alias for `get_value()` * * @return mixed Returns the value of the target setting or null if the setting parameter is not found. The type of the returned var depends on the setting's `type` field. */ + public static function get(string $package, string $section, string $code, $default=null, array $selector=[], string $lang=null) { + return self::get_value($package, $section, $code, $default, $selector, $lang); + } + public static function get_value(string $package, string $section, string $code, $default=null, array $selector=[], string $lang=null) { $result = $default; @@ -230,18 +235,30 @@ public static function get_value(string $package, string $section, string $code, } $setting_values = $om->read(SettingValue::getType(), $setting['setting_values_ids'], ['user_id', 'value'], $values_lang); - if($setting_values > 0) { + if($setting_values > 0 && count($setting_values)) { $value = null; // #memo - by default settings values are sorted on user_id (which can be null), so first value is the default one foreach($setting_values as $setting_value) { $value = $setting_value['value']; - if(isset($selector['user_id']) && $setting_value['user_id'] == $selector['user_id']) { + if(isset($selector['user_id']) && isset($setting_value['user_id']) && $setting_value['user_id'] == $selector['user_id']) { break; } } if(!is_null($value)) { $result = $value; - settype($result, $setting['type']); + + $map_types = [ + 'boolean' => 'boolean', + 'integer' => 'integer', + 'float' => 'double', + 'string' => 'string', + 'many2one' => 'integer' + ]; + + settype($result, $map_types[$setting['type']]); + } + elseif($setting['type'] == 'many2one') { + $result = null; } } } @@ -324,7 +341,7 @@ public static function fetch_and_add(string $package, string $section, string $c $result = null; $providers = \eQual::inject(['orm']); - /** @var \equal\orm\ObjectManager */ + /** @var \equal\orm\ObjectManager $orm */ $orm = $providers['orm']; $settings_ids = $orm->search(self::getType(), [ diff --git a/packages/core/data/envinfo.php b/packages/core/data/envinfo.php index 8b2c28d86..a0f8dd86f 100644 --- a/packages/core/data/envinfo.php +++ b/packages/core/data/envinfo.php @@ -28,7 +28,8 @@ "APP_LOGO_URL", "BACKEND_URL", "REST_API_URL", - "NOTIFICATIONS_ENABLED" + "NOTIFICATIONS_ENABLED", + "USER_ACCOUNT_REGISTRATION" ], 'providers' => ['context', 'auth'] ] ); @@ -36,17 +37,18 @@ list($context, $auth) = [$providers['context'], $providers['auth']]; $envinfo = [ - "env_mode" => constant('ENV_MODE'), - "production" => (constant('ENV_MODE') == 'production'), - "parent_domain" => parse_url(constant('BACKEND_URL'), PHP_URL_HOST), - "backend_url" => constant('BACKEND_URL'), - "rest_api_url" => constant('REST_API_URL'), - "lang" => constant('APP_DEFAULT_LANG'), - "locale" => constant('L10N_LOCALE'), - "company_name" => constant('ORG_NAME'), - "company_url" => constant('ORG_URL'), - "app_name" => constant('APP_NAME'), - "app_logo_url" => constant('APP_LOGO_URL') + "env_mode" => constant('ENV_MODE'), + "production" => (constant('ENV_MODE') == 'production'), + "parent_domain" => parse_url(constant('BACKEND_URL'), PHP_URL_HOST), + "backend_url" => constant('BACKEND_URL'), + "rest_api_url" => constant('REST_API_URL'), + "lang" => constant('APP_DEFAULT_LANG'), + "locale" => constant('L10N_LOCALE'), + "company_name" => constant('ORG_NAME'), + "company_url" => constant('ORG_URL'), + "app_name" => constant('APP_NAME'), + "app_logo_url" => constant('APP_LOGO_URL'), + "account_registration" => constant('USER_ACCOUNT_REGISTRATION') ]; // retrieve current User diff --git a/packages/core/i18n/en/setting/Setting.json b/packages/core/i18n/en/setting/Setting.json index a6338a57d..1f8c93c77 100644 --- a/packages/core/i18n/en/setting/Setting.json +++ b/packages/core/i18n/en/setting/Setting.json @@ -41,10 +41,11 @@ "type": { "label": "Type", "selection": { - "boolean": "booléen", - "integer": "entier", - "float": "nombre flottant", - "string": "chaîne de caractères" + "boolean": "boolean", + "integer": "integer", + "float": "float", + "string": "string", + "many2one": "relation" }, "description": "Setting type.", "help": "" diff --git a/packages/core/i18n/fr/setting/Setting.json b/packages/core/i18n/fr/setting/Setting.json index 7ffbf5998..309f546f1 100644 --- a/packages/core/i18n/fr/setting/Setting.json +++ b/packages/core/i18n/fr/setting/Setting.json @@ -50,8 +50,9 @@ "selection":{ "boolean": "booléen", "integer": "entier", - "float": "nombre flottant", - "string": "chaîne de caractère" + "float": "réel (flottant)", + "string": "chaîne de caractères", + "many2one": "relation" } } }, diff --git a/packages/core/views/setting/Setting.form.default.json b/packages/core/views/setting/Setting.form.default.json index b3057fdb8..884d623d8 100644 --- a/packages/core/views/setting/Setting.form.default.json +++ b/packages/core/views/setting/Setting.form.default.json @@ -64,6 +64,11 @@ "value": "form_control", "width": "50%" }, + { + "type": "field", + "value": "object_class", + "width": "100%" + }, { "type": "field", "value": "is_multilang", diff --git a/public/assets/img/equal_presentation.gif b/public/assets/img/equal_presentation.gif new file mode 100644 index 000000000..56659b598 Binary files /dev/null and b/public/assets/img/equal_presentation.gif differ diff --git a/public/console.php b/public/console.php index 88b45b85f..3c7da6576 100644 --- a/public/console.php +++ b/public/console.php @@ -7,6 +7,7 @@ License: GNU LGPL 3 license */ error_reporting(0); +define('MAX_FILESIZE', 100 * 1000 * 1000); // get log file, using variation from URL, if any $log_file = (isset($_GET['f']) && strlen($_GET['f'])) ? $_GET['f'] : 'equal.log'; @@ -18,7 +19,7 @@ } -// no param given : frond-end App provider +// no param given: frond-end App provider if(!count($_GET)) { echo ' @@ -215,7 +216,7 @@ #header { position: fixed; top: 0; - height: 105px; + height: 135px; width: 100%; background: white; z-index: 4; @@ -230,7 +231,7 @@ } #start { - padding-top: 110px; + padding-top: 135px; } .loader-overlay { @@ -431,6 +432,20 @@ overflow: hidden !important; white-space: break-spaces; } + + button.btn { + height: 18px; + border: none !important; + border-radius: 0 !important; + outline: 0 !important; + padding: 2px 10px; + font-size: 11px; + opacity: 0.5; + } + + button.btn.applied { + opacity: 1; + }