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;
+ }