diff --git a/.gitignore b/.gitignore index e1ca2a9fa..3c7be4fad 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,7 @@ config/routing/* composer.phar composer.lock .vscode +.idea public/* !public/index.php !public/console.php diff --git a/config/config-example_mysql.json b/config/config-example_mysql.json index 6d7f505f4..18f3a2e1c 100644 --- a/config/config-example_mysql.json +++ b/config/config-example_mysql.json @@ -1,8 +1,8 @@ { "ROUTING_METHOD": "JSON", "FILE_STORAGE_MODE":"DB", - "DEFAULT_RIGHTS": "QN_R_CREATE | QN_R_READ | QN_R_DELETE | QN_R_WRITE", - "DEBUG_MODE": "QN_MODE_PHP | QN_MODE_ORM | QN_MODE_SQL", + "DEFAULT_RIGHTS": "EQ_R_CREATE | EQ_R_READ | EQ_R_DELETE | EQ_R_WRITE", + "DEBUG_MODE": "EQ_MODE_API | EQ_MODE_PHP | EQ_MODE_ORM | EQ_MODE_SQL", "DEBUG_LEVEL": "E_ALL | E_ALL", "DEFAULT_LANG": "en", "DB_REPLICATION": "NO", diff --git a/config/config-example_sqlsrv.json b/config/config-example_sqlsrv.json index a8592af07..5c1181d4b 100644 --- a/config/config-example_sqlsrv.json +++ b/config/config-example_sqlsrv.json @@ -2,8 +2,8 @@ "CIPHER_KEY": "xxxxxxxxxxxxxxxxxxxxxxxx", "ROUTING_METHOD": "JSON", "FILE_STORAGE_MODE":"DB", - "DEFAULT_RIGHTS": "QN_R_CREATE | QN_R_READ | QN_R_DELETE | QN_R_WRITE", - "DEBUG_MODE": "QN_MODE_PHP | QN_MODE_ORM | QN_MODE_SQL", + "DEFAULT_RIGHTS": "EQ_R_CREATE | EQ_R_READ | EQ_R_DELETE | EQ_R_WRITE", + "DEBUG_MODE": "EQ_MODE_PHP | EQ_MODE_ORM | EQ_MODE_SQL", "DEBUG_LEVEL": "E_ALL | E_ALL", "DEFAULT_LANG": "en", "DB_REPLICATION": "NO", diff --git a/config/schema.json b/config/schema.json index 387b40478..36deabb63 100644 --- a/config/schema.json +++ b/config/schema.json @@ -15,7 +15,7 @@ }, "DEFAULT_LANG": { "type": "string", - "description": "The language in which the multilang content must be provided/stored by default (ISO 639-1).", + "description": "The language (ISO 639-1) in which the multilang content must be provided/stored by default.", "instant": true, "default": "en" }, @@ -50,8 +50,8 @@ "default": 0, "examples": [ 0, - "QN_R_READ | QN_R_WRITE", - "QN_R_CREATE | QN_R_READ | QN_R_DELETE | QN_R_WRITE | QN_R_MANAGE" + "EQ_R_READ | EQ_R_WRITE", + "EQ_R_CREATE | EQ_R_READ | EQ_R_DELETE | EQ_R_WRITE | EQ_R_MANAGE" ] }, "DEBUG_MODE": { @@ -60,16 +60,18 @@ "instant": true, "default": 0, "examples": [ - "QN_MODE_PHP | QN_MODE_ORM | QN_MODE_SQL | QN_MODE_APP | QN_MODE_API" + "EQ_MODE_PHP | EQ_MODE_ORM | EQ_MODE_SQL | EQ_MODE_APP | EQ_MODE_API | EQ_MODE_AAA | EQ_MODE_NET" ] }, "DEBUG_LEVEL": { "type": "integer", - "description": "Types of error to report (defaults to E_ALL = 32767, setting to 0 means no logs). Important: if logging level is set to E_ALL with all mode enabled, then the log file will grow quickly.", + "description": "Types of error to report (defaults to E_ALL = 32767, setting to 0 means no logs). Important: if logging level is set to E_ALL with all mode enabled, then the log file grows quite quickly.", "instant": true, "default": 0, "examples": [ - "QN_REPORT_DEBUG | QN_REPORT_INFO | QN_REPORT_WARNING | QN_REPORT_ERROR | QN_REPORT_FATAL" + "EQ_REPORT_DEBUG | EQ_REPORT_INFO | EQ_REPORT_WARNING | EQ_REPORT_ERROR | EQ_REPORT_FATAL", + "EQ_REPORT_INFO | EQ_REPORT_WARNING | EQ_REPORT_ERROR", + "EQ_REPORT_DEBUG & E_ALL" ] }, "HTTP_REQUEST_TIMEOUT": { @@ -282,8 +284,12 @@ }, "USER_ACCOUNT_DISPLAYNAME": { "type": "string", - "description": "Strategy for displaying the user name to other users. The expected value is a string holding one or more of following references: 'id', 'nickname', 'mail', 'givenname', 'surname', 'initials' (Note: 'mail' is the email address and suits Apps restricted to users from a same organisation but should be avoided for non public profiles).", - "default": "mail" + "description": "Strategy for displaying the user name to other users. The expected value is a string holding one or more of following references: 'id', 'nickname', 'mail', 'firstname', 'lastname', 'initials' (Note: 'mail' is the email address and suits Apps restricted to users from a same organisation but should be avoided for non public profiles).", + "default": "mail", + "examples": [ + "nickname", + "firstname lastname" + ] }, "USER_ACCOUNT_REGISTRATION": { "type": "boolean", diff --git a/doc b/doc index 6f04d0ad5..a50423244 160000 --- a/doc +++ b/doc @@ -1 +1 @@ -Subproject commit 6f04d0ad57feb1c34b31738381ff9da7030dcc2c +Subproject commit a504232442ffb7acc2c669b7cab848e134978c88 diff --git a/eq.lib.php b/eq.lib.php index c2ad543a9..91d2a6994 100644 --- a/eq.lib.php +++ b/eq.lib.php @@ -38,56 +38,78 @@ /** * Root directory of current install */ - define('QN_BASEDIR', realpath(dirname(__FILE__))); + define('EQ_BASEDIR', realpath(dirname(__FILE__))); + // equivalence for constant names migration + // #deprecated + define('QN_BASEDIR', EQ_BASEDIR); /** * Error codes * we use negative values to make it possible to distinguish error codes from object ids */ - define('QN_ERROR_UNKNOWN', -1); // something went wrong (check the logs) - define('QN_ERROR_MISSING_PARAM', -2); // one or more mandatory parameters are missing - define('QN_ERROR_INVALID_PARAM', -4); // one or more parameters have invalid or incompatible value - define('QN_ERROR_SQL', -8); // error while building SQL query or processing it (check that object class matches DB schema) - define('QN_ERROR_UNKNOWN_OBJECT', -16); // unknown resource (class, object, view, ...) - define('QN_ERROR_NOT_ALLOWED', -32); // action violates some rule (including UPLOAD_MAX_FILE_SIZE for binary fields) or user don't have required permissions - define('QN_ERROR_LOCKED_OBJECT', -64); // object is currently locked by another process - define('QN_ERROR_CONFLICT_OBJECT', -128); // version conflict - define('QN_ERROR_INVALID_USER', -256); // auth failure - define('QN_ERROR_UNKNOWN_SERVICE', -512); // server error : missing service - define('QN_ERROR_INVALID_CONFIG', -1024); // server error : faulty configuration - - + define('EQ_ERROR_UNKNOWN', -1); // something went wrong (check the logs) + define('EQ_ERROR_MISSING_PARAM', -2); // one or more mandatory parameters are missing + define('EQ_ERROR_INVALID_PARAM', -4); // one or more parameters have invalid or incompatible value + define('EQ_ERROR_SQL', -8); // error while building SQL query or processing it (check that object class matches DB schema) + define('EQ_ERROR_UNKNOWN_OBJECT', -16); // unknown resource (class, object, view, ...) + define('EQ_ERROR_NOT_ALLOWED', -32); // action violates some rule (including UPLOAD_MAX_FILE_SIZE for binary fields) or user don't have required permissions + define('EQ_ERROR_LOCKED_OBJECT', -64); // object is currently locked by another process + define('EQ_ERROR_CONFLICT_OBJECT', -128); // version conflict + define('EQ_ERROR_INVALID_USER', -256); // auth failure + define('EQ_ERROR_UNKNOWN_SERVICE', -512); // server error : missing service + define('EQ_ERROR_INVALID_CONFIG', -1024); // server error : faulty configuration // equivalence map for constant names migration - define('EQ_ERROR_UNKNOWN', QN_ERROR_UNKNOWN); - define('EQ_ERROR_MISSING_PARAM', QN_ERROR_MISSING_PARAM); - define('EQ_ERROR_INVALID_PARAM', QN_ERROR_INVALID_PARAM); - define('EQ_ERROR_SQL', QN_ERROR_SQL); - define('EQ_ERROR_UNKNOWN_OBJECT', QN_ERROR_UNKNOWN_OBJECT); - define('EQ_ERROR_NOT_ALLOWED', QN_ERROR_NOT_ALLOWED); - define('EQ_ERROR_LOCKED_OBJECT', QN_ERROR_LOCKED_OBJECT); - define('EQ_ERROR_CONFLICT_OBJECT', QN_ERROR_CONFLICT_OBJECT); - define('EQ_ERROR_INVALID_USER', QN_ERROR_INVALID_USER); - define('EQ_ERROR_UNKNOWN_SERVICE', QN_ERROR_UNKNOWN_SERVICE); - define('EQ_ERROR_INVALID_CONFIG', QN_ERROR_INVALID_CONFIG); + // #deprecated + define('QN_ERROR_UNKNOWN', EQ_ERROR_UNKNOWN); + define('QN_ERROR_MISSING_PARAM', EQ_ERROR_MISSING_PARAM); + define('QN_ERROR_INVALID_PARAM', EQ_ERROR_INVALID_PARAM); + define('QN_ERROR_SQL', EQ_ERROR_SQL); + define('QN_ERROR_UNKNOWN_OBJECT', EQ_ERROR_UNKNOWN_OBJECT); + define('QN_ERROR_NOT_ALLOWED', EQ_ERROR_NOT_ALLOWED); + define('QN_ERROR_LOCKED_OBJECT', EQ_ERROR_LOCKED_OBJECT); + define('QN_ERROR_CONFLICT_OBJECT', EQ_ERROR_CONFLICT_OBJECT); + define('QN_ERROR_INVALID_USER', EQ_ERROR_INVALID_USER); + define('QN_ERROR_UNKNOWN_SERVICE', EQ_ERROR_UNKNOWN_SERVICE); + define('QN_ERROR_INVALID_CONFIG', EQ_ERROR_INVALID_CONFIG); /** * Debugging modes and levels */ // debugging modes - define('QN_MODE_PHP', 1); - define('QN_MODE_SQL', 2); - define('QN_MODE_ORM', 4); - define('QN_MODE_API', 8); - define('QN_MODE_APP', 16); + define('EQ_MODE_PHP', 1); // low-level logs (code) + define('EQ_MODE_SQL', 2); // DB related logs + define('EQ_MODE_ORM', 4); // ORM entities & manipulations logs + define('EQ_MODE_API', 8); // routes & controllers related logs + define('EQ_MODE_APP', 16); // application logic logs + define('EQ_MODE_AAA', 32); // authentication, authorization & accounting logs + define('EQ_MODE_NET', 64); // network logs (tcp/ip, http) + // equivalence map for constant names migration + // #deprecated + define('QN_MODE_PHP', EQ_MODE_PHP); + define('QN_MODE_SQL', EQ_MODE_SQL); + define('QN_MODE_ORM', EQ_MODE_ORM); + define('QN_MODE_API', EQ_MODE_API); + define('QN_MODE_APP', EQ_MODE_APP); + define('QN_MODE_AAA', EQ_MODE_AAA); + define('QN_MODE_NET', EQ_MODE_NET); // debugging levels - define('QN_REPORT_DEBUG', E_USER_DEPRECATED); // 16384 - define('QN_REPORT_INFO', E_USER_NOTICE); // 1024 - define('QN_REPORT_WARNING', E_USER_WARNING); // 512 - define('QN_REPORT_ERROR', E_USER_ERROR); // 256 - define('QN_REPORT_FATAL', E_ERROR); // 1 + define('EQ_REPORT_DEBUG', E_USER_DEPRECATED); // 16384 + define('EQ_REPORT_INFO', E_USER_NOTICE); // 1024 + define('EQ_REPORT_WARNING', E_USER_WARNING); // 512 + define('EQ_REPORT_ERROR', E_USER_ERROR); // 256 + define('EQ_REPORT_FATAL', E_ERROR); // 1 + define('EQ_REPORT_SYSTEM', 0); // 0 + // equivalence map for constant names migration + // #deprecated + define('QN_REPORT_DEBUG', EQ_REPORT_DEBUG); + define('QN_REPORT_INFO', EQ_REPORT_INFO); + define('QN_REPORT_WARNING', EQ_REPORT_WARNING); + define('QN_REPORT_ERROR', EQ_REPORT_ERROR); + define('QN_REPORT_FATAL', EQ_REPORT_FATAL); + define('QN_REPORT_SYSTEM', EQ_REPORT_SYSTEM); /** * Logs storage directory @@ -204,6 +226,7 @@ function qn_debug_code_name($code) { case QN_REPORT_WARNING: return 'WARNING'; case QN_REPORT_ERROR: return 'ERROR'; case QN_REPORT_FATAL: return 'FATAL'; + case QN_REPORT_SYSTEM: return 'SYSTEM'; } return 'UNKNOWN'; } @@ -215,6 +238,8 @@ function qn_debug_mode_name($mode) { case QN_MODE_ORM: return 'ORM'; case QN_MODE_API: return 'API'; case QN_MODE_APP: return 'APP'; + case QN_MODE_AAA: return 'AAA'; + case QN_MODE_NET: return 'NET'; } return 'UNKNOWN'; } @@ -504,12 +529,18 @@ public static function init() { 'route' => 'equal\route\Router', 'log' => 'equal\log\Logger', 'cron' => 'equal\cron\Scheduler', - 'dispatch' => 'equal\dispatch\Dispatcher' + 'dispatch' => 'equal\dispatch\Dispatcher', + 'db' => 'equal\db\DBConnector' ]); - // make sure mandatory dependencies are available (reporter requires context) try { + // make mandatory dependencies available $container->get(['report', 'context']); + // register ORM classes auto-loader + $om = $container->get('orm'); + // init collections provider + $container->get('equal\orm\Collections'); + spl_autoload_register([$om, 'getModel']); } catch(\Throwable $e) { // fallback to a manual HTTP 500 @@ -522,17 +553,6 @@ public static function init() { // and raise an exception (will be output in PHP error log) throw new \Exception("missing_mandatory_dependency", QN_REPORT_FATAL); } - // register ORM classes autoloader - try { - $om = $container->get('orm'); - // init collections provider - $container->get('equal\orm\Collections'); - spl_autoload_register([$om, 'getModel']); - } - catch(\Throwable $e) { - throw new \Exception("autoload_register_failed", QN_REPORT_FATAL); - } - } public static function getLastContext() { @@ -586,7 +606,7 @@ public static function announce(array $announcement) { // set Response default Content Type to JSON $response->headers()->setContentType('application/json'); - $reporter->debug("method $method"); + $reporter->debug("API::method: $method"); // normalize $announcement array if(!isset($announcement['params'])) { @@ -722,14 +742,14 @@ public static function announce(array $announcement) { $expires = intval($announcement['response']['expires']); $age = time() - filemtime(realpath($cache_filename)); if($age >= $expires) { - $reporter->debug("expired cache-id {$cache_id}"); + $reporter->debug("API::expired cache-id {$cache_id}"); $serve_from_cache = false; } } // handle manual request for invalidating the cache if(isset($body['cache'])) { if(in_array($body['cache'], [null, false, 0, '0'])) { - $reporter->debug("manual reset cache-id {$cache_id}"); + $reporter->debug("API::manual reset cache-id {$cache_id}"); $serve_from_cache = false; } // cache is a reserved parameter: no further process @@ -739,7 +759,7 @@ public static function announce(array $announcement) { if(file_exists($cache_filename)) { // cache was invalidated: remove related file if(!$serve_from_cache) { - $reporter->debug("invalidating cache-id {$cache_id}"); + $reporter->debug("API::invalidating cache-id {$cache_id}"); unlink($cache_filename); } // cache is still valid: serve from cache @@ -752,7 +772,7 @@ public static function announce(array $announcement) { ->send(); throw new \Exception('', 0); } - $reporter->debug("serving from cache-id {$cache_id}"); + $reporter->debug("API::serving from cache-id {$cache_id}"); list($headers, $result) = unserialize(file_get_contents($cache_filename)); // build response with cached headers foreach($headers as $header => $value) { @@ -902,7 +922,7 @@ public static function announce(array $announcement) { $allowed_params = array_keys($announcement['params']); $unknown_params = array_diff(array_keys($body), $allowed_params); foreach($unknown_params as $unknown_param) { - $reporter->debug("dropped unexpected parameter '{$unknown_param}'"); + $reporter->debug("API::dropped unexpected parameter '{$unknown_param}'"); unset($body[$unknown_param]); } $missing_params = array_diff($allowed_params, array_intersect($allowed_params, array_keys($body))); @@ -941,11 +961,11 @@ public static function announce(array $announcement) { } else { if(isset($config['default'])) { - $reporter->warning("invalid value for non-mandatory parameter '{$param}' reverted to default '{$config['default']}'"); + $reporter->warning("API::invalid value for non-mandatory parameter '{$param}' reverted to default '{$config['default']}'"); $result[$param] = $config['default']; } else { - $reporter->warning("dropped invalid non-mandatory parameter '{$param}'"); + $reporter->warning("API::dropped invalid non-mandatory parameter '{$param}'"); } } } @@ -974,12 +994,12 @@ public static function announce(array $announcement) { if(!in_array($param, $mandatory_params)) { // if it has a default value, assign to it if(isset($config['default'])) { - $reporter->warning("invalid value {$value} for non-mandatory parameter '{$param}' reverted to default '{$config['default']}'"); + $reporter->warning("API::invalid value {$value} for non-mandatory parameter '{$param}' reverted to default '{$config['default']}'"); $result[$param] = $config['default']; } else { // otherwise, drop it - $reporter->warning("dropped invalid non-mandatory parameter '{$param}'"); + $reporter->warning("API::dropped invalid non-mandatory parameter '{$param}'"); unset($result[$param]); } } @@ -990,7 +1010,7 @@ public static function announce(array $announcement) { } // report received parameters - $reporter->debug("params: ".json_encode($result)); + $reporter->debug("API::params: ".json_encode($result)); if(count($invalid_params)) { // no feedback about services @@ -1059,8 +1079,13 @@ public static function announce(array $announcement) { * @example run('get', 'model_read', ['entity' => 'core\Group', 'id'=> 1]); */ public static function run($type, $operation, $body=[], $root=false) { - trigger_error("PHP::calling run method for $type:$operation", QN_REPORT_DEBUG); global $last_context; + /** @var \equal\services\Container */ + $container = Container::getInstance(); + /** @var \equal\error\Reporter */ + $reporter = $container->get('report'); + + $reporter->info("API::operation: $type:$operation"); $result = ''; $resolved = [ @@ -1076,7 +1101,6 @@ public static function run($type, $operation, $body=[], $root=false) { 'get' => ['kind' => 'DATA_PROVIDER', 'dir' => 'data' ], // return some data 'show' => ['kind' => 'APPLICATION', 'dir' => 'apps' ] // output rendering information (UI) ]; - $container = Container::getInstance(); if(!$root) { $context_orig = $container->get('context'); @@ -1093,9 +1117,6 @@ public static function run($type, $operation, $body=[], $root=false) { $context = $container->get('context'); } - /** @var \equal\error\Reporter */ - $reporter = $container->get('report'); - $getOperationOutput = function($script) use($context) { ob_start(); try { @@ -1133,6 +1154,12 @@ public static function run($type, $operation, $body=[], $root=false) { $request = $context->httpRequest(); $request->body($body); + $reporter->info("API::".json_encode([ + 'uri' => (string) $request->getUri(), + 'headers' => $request->getHeaders(true), + 'body' => $request->getBody() + ], JSON_PRETTY_PRINT)); + $operation = explode(':', $operation); if(count($operation) > 1) { $visibility = array_shift($operation); @@ -1151,12 +1178,6 @@ public static function run($type, $operation, $body=[], $root=false) { } } - $reporter->debug(json_encode([ - 'uri' => (string) $request->getUri(), - 'headers' => $request->getHeaders(true), - 'body' => $request->getBody() - ], JSON_PRETTY_PRINT)); - // load package custom configuration, if any if(!is_null($resolved['package']) && is_file(QN_BASEDIR.'/packages/'.$resolved['package'].'/config.json')) { $data = file_get_contents(QN_BASEDIR.'/packages/'.$resolved['package'].'/config.json'); @@ -1233,10 +1254,10 @@ public static function run($type, $operation, $body=[], $root=false) { $context->httpResponse()->header('Etag', $cache_id); $headers = $context->httpResponse()->headers()->toArray(); file_put_contents(QN_BASEDIR.'/cache/'.$cache_id, serialize([$headers, $result])); - $reporter->debug("stored cache-id {$cache_id}"); + $reporter->debug("API::stored cache-id {$cache_id}"); } } - trigger_error("PHP::result: $result", QN_REPORT_DEBUG); + trigger_error("API::result: $result", QN_REPORT_DEBUG); } // restore context @@ -1289,7 +1310,7 @@ public static function load_class($class_name) { $result = include_once $file_path.'.class.php'; } // Fallback to simple php extension - else if(file_exists($file_path.'.php')) { + elseif(file_exists($file_path.'.php')) { $result = include_once $file_path.'.php'; } else { @@ -1303,7 +1324,7 @@ public static function load_class($class_name) { } - // Initialize the eQual class for further 'load_class' calls + // bootstrap eQual eQual::init(); } namespace { diff --git a/lib/equal/db/DBConnection.class.php b/lib/equal/db/DBConnection.class.php index 04a7de2b5..561d9ff6d 100644 --- a/lib/equal/db/DBConnection.class.php +++ b/lib/equal/db/DBConnection.class.php @@ -1,58 +1,32 @@ - Some Rights Reserved, Cedric Francoys, 2010-2021 - Licensed under GNU LGPL 3 license + This file is part of the eQual framework + Some Rights Reserved, eQual framework, 2010-2024 + Original author(s): Cédric FRANCOYS + License: GNU LGPL 3 license */ namespace equal\db; -use equal\organic\Service; /** - * Service implementing factory pattern for DBManipulator instances. + * This class uses factory pattern for providing DBManipulator instances. */ -class DBConnection extends Service { +class DBConnection { - /** - * @var DBManipulator - */ - private $dbConnection; + public static function create(string $dbms='', string $host='', int $port=null, string $name='', string $user='', string $password='', string $charset='', string $collation='') { + /** @var DBManipulator */ + $dbConnection = null; - protected function __construct() { - switch(constant('DB_DBMS')) { + switch($dbms) { case 'MARIADB': case 'MYSQL' : - $this->dbConnection = new DBManipulatorMySQL( - constant('DB_HOST'), - constant('DB_PORT'), - constant('DB_NAME'), - constant('DB_USER'), - constant('DB_PASSWORD'), - constant('DB_CHARSET'), - constant('DB_COLLATION') - ); + $dbConnection = new DBManipulatorMySQL($host, $port, $name, $user, $password, $charset, $collation); break; case 'SQLSRV' : - $this->dbConnection = new DBManipulatorSqlSrv( - constant('DB_HOST'), - constant('DB_PORT'), - constant('DB_NAME'), - constant('DB_USER'), - constant('DB_PASSWORD'), - constant('DB_CHARSET'), - constant('DB_COLLATION') - ); + $dbConnection = new DBManipulatorSqlSrv($host, $port, $name, $user, $password, $charset, $collation); break; case 'SQLITE' : - $this->dbConnection = new DBManipulatorSQLite( - constant('DB_HOST'), - constant('DB_PORT'), - constant('DB_NAME'), - constant('DB_USER'), - constant('DB_PASSWORD'), - constant('DB_CHARSET'), - constant('DB_COLLATION') - ); + $dbConnection = new DBManipulatorSQLite($host, $port, $name, $user, $password, $charset, $collation); break; case 'POSTGRESQL' : // #todo @@ -60,80 +34,8 @@ protected function __construct() { case 'ORACLE' : // #todo break; - default: - $this->dbConnection = null; } - - if(defined('DB_REPLICATION') && constant('DB_REPLICATION') != 'NO') { - // add replica members, if any - $i = 1; - - while(defined('DB_'.$i.'_HOST') - && defined('DB_'.$i.'_PORT') - && defined('DB_'.$i.'_USER') - && defined('DB_'.$i.'_PASSWORD') - && defined('DB_'.$i.'_NAME')) { - - $this->addReplicaMember( - constant('DB_'.$i.'_HOST'), - constant('DB_'.$i.'_PORT'), - constant('DB_'.$i.'_NAME'), - constant('DB_'.$i.'_USER'), - constant('DB_'.$i.'_PASSWORD') - ); - ++$i; - } - } - } - - public static function constants() { - return ['DB_HOST', 'DB_PORT', 'DB_NAME', 'DB_USER', 'DB_PASSWORD', 'DB_DBMS', 'DB_CHARSET', 'DB_COLLATION']; - } - - public function addReplicaMember($host, $port, $db, $user, $pass) { - $member = null; - switch(constant('DB_DBMS')) { - case 'MYSQL' : - $member = new DBManipulatorMySQL($host, $port, $db, $user, $pass); - break; - /* - // insert handling of other DBMS here - case 'XYZ' : - $this->dbConnection = new DBManipulatorXyz(DB_HOST, DB_PORT, DB_NAME, DB_USER, DB_PASSWORD); - break; - */ - default: - break; - } - if($member && $this->dbConnection) { - $this->dbConnection->addMember($member); - } - } - - public function connect($auto_select=true) { - if(!isset($this->dbConnection)) return false; - return $this->dbConnection->connect($auto_select); - } - - public function disconnect() { - if(!isset($this->dbConnection)) return true; - return $this->dbConnection->disconnect(); - } - - /** - * Magic overloading method: catch any call and relay it to DBConnection object - * - * @param string $name - * @param array $arguments - * @return mixed - */ - public function __call($name, $arguments) { - - if (!$this->dbConnection) { - return null; - } - - return call_user_func_array([$this->dbConnection, $name], $arguments); + return $dbConnection; } } diff --git a/lib/equal/db/DBConnector.class.php b/lib/equal/db/DBConnector.class.php new file mode 100644 index 000000000..449835341 --- /dev/null +++ b/lib/equal/db/DBConnector.class.php @@ -0,0 +1,89 @@ + + Some Rights Reserved, eQual framework, 2010-2024 + Original author(s): Cédric FRANCOYS + License: GNU LGPL 3 license +*/ +namespace equal\db; + +use equal\organic\Service; + +/** + * Service for connecting to the DBMS holding the database of the current installation. + * This service acts as a facade for DB interactions. + */ +class DBConnector extends Service { + + /** @var DBManipulator */ + private $connection; + + public static function constants() { + return ['DB_DBMS', 'DB_HOST', 'DB_PORT', 'DB_NAME', 'DB_USER', 'DB_PASSWORD', 'DB_CHARSET', 'DB_COLLATION']; + } + + protected function __construct() { + $this->connection = DBConnection::create( + constant('DB_DBMS'), + constant('DB_HOST'), + constant('DB_PORT'), + constant('DB_NAME'), + constant('DB_USER'), + constant('DB_PASSWORD'), + constant('DB_CHARSET'), + constant('DB_COLLATION') + ); + + if(defined('DB_REPLICATION') && constant('DB_REPLICATION') != 'NO') { + // add replica members, if any + $i = 1; + + while(defined('DB_'.$i.'_HOST') + && defined('DB_'.$i.'_PORT') + && defined('DB_'.$i.'_USER') + && defined('DB_'.$i.'_PASSWORD') + && defined('DB_'.$i.'_NAME')) { + + $this->addReplicaMember( + constant('DB_'.$i.'_HOST'), + constant('DB_'.$i.'_PORT'), + constant('DB_'.$i.'_NAME'), + constant('DB_'.$i.'_USER'), + constant('DB_'.$i.'_PASSWORD') + ); + ++$i; + } + } + } + + public function addReplicaMember($host, $port, $db, $user, $pass) { + /** @var DBManipulator */ + $member = DBConnection::create(constant('DB_DBMS'), $host, $port, $db, $user, $pass); + if($member && $this->connection) { + $this->connection->addMember($member); + } + } + + public function connect($auto_select=true) { + return isset($this->connection)?$this->connection->connect($auto_select):false; + } + + public function disconnect() { + return !isset($this->connection) || $this->connection->disconnect(); + } + + /** + * Magic overloading method: catch any call and relay it to DBManipulator object + * + * @param string $name + * @param array $arguments + * @return mixed + */ + public function __call($name, $arguments) { + if (!$this->connection) { + return null; + } + return call_user_func_array([$this->connection, $name], $arguments); + } + +} diff --git a/lib/equal/db/DBManipulator.class.php b/lib/equal/db/DBManipulator.class.php index 6c541c069..95870ff18 100644 --- a/lib/equal/db/DBManipulator.class.php +++ b/lib/equal/db/DBManipulator.class.php @@ -6,6 +6,9 @@ */ namespace equal\db; +/** + * This class is used as abstract class providing members and methods signature for DBManipulator class that extend it. + */ class DBManipulator { /** @@ -136,6 +139,7 @@ public function addMember(DBManipulator $member) { * @access public */ public function connect($auto_select=true) { + return $this; } public function select($db_name) { @@ -206,6 +210,14 @@ public function getLastQuery() { return $this->last_query; } + public static function fetchRow($result) { + return []; + } + + public static function fetchArray($result) { + return []; + } + protected function setLastId($id) { $this->last_id = $id; } @@ -218,4 +230,42 @@ protected function setLastQuery($query) { $this->last_query = $query; } + /** + * Get records from specified table, according to some conditions. + * + * @param array $tables name of involved tables + * @param array $fields list of requested fields + * @param array $ids ids to which the selection is limited + * @param array $conditions list of arrays (field, operand, value) + * @param string $id_field name of the id field ('id' by default) + * @param mixed $order string holding name of the order field or maps holding field names as keys and sorting as value + * @param integer $start + * @param integer $limit + * + * @return resource reference to query resource + */ + public function getRecords($tables, $fields=NULL, $ids=NULL, $conditions=NULL, $id_field='id', $order=[], $start=0, $limit=0) {} + + public function setRecords($table, $ids, $fields, $conditions=null, $id_field='id') {} + + /** + * Inserts new records in specified table. + * + * @param string $table name of the table in which insert the records + * @param array $fields list of involved fields + * @param array $values array of arrays specifying the values related to each specified field + * @return resource reference to query resource + */ + public function addRecords($table, $fields, $values) {} + + public function deleteRecords($table, $ids, $conditions=null, $id_field='id') {} + + /** + * Fetch and increment the column of a series of records in a single operation. + * This method implements FAA instruction (fetch-and-add) in order to read and update a column as an atomic operation. + * + * @param int $increment A numeric value used to increment columns (if value positive) or decrement columns (if value is negative). + */ + public function incRecords($table, $ids, $field, $increment, $id_field='id') { + } } diff --git a/lib/equal/db/DBManipulatorMySQL.class.php b/lib/equal/db/DBManipulatorMySQL.class.php index 1546ccd93..b3e68d177 100644 --- a/lib/equal/db/DBManipulatorMySQL.class.php +++ b/lib/equal/db/DBManipulatorMySQL.class.php @@ -10,8 +10,7 @@ * DBManipulator implementation for MySQL server. * */ - -class DBManipulatorMySQL extends DBManipulator { +final class DBManipulatorMySQL extends DBManipulator { public static $types_associations = [ @@ -405,20 +404,6 @@ private function getConditionClause($id_field, $ids, $conditions) { return $sql; } - /** - * Get records from specified table, according to some conditions. - * - * @param array $tables name of involved tables - * @param array $fields list of requested fields - * @param array $ids ids to which the selection is limited - * @param array $conditions list of arrays (field, operand, value) - * @param string $id_field name of the id field ('id' by default) - * @param mixed $order string holding name of the order field or maps holding field nmaes as keys and sorting as value - * @param integer $start - * @param integer $limit - * - * @return resource reference to query resource - */ public function getRecords($tables, $fields=NULL, $ids=NULL, $conditions=NULL, $id_field='id', $order=[], $start=0, $limit=0) { // cast tables to an array (passing a single table is accepted) if(!is_array($tables)) { @@ -435,7 +420,7 @@ public function getRecords($tables, $fields=NULL, $ids=NULL, $conditions=NULL, $ // test values and types if(empty($tables)) { - throw new \Exception(__METHOD__." : unable to build sql query, parameter 'tables' array is empty.", QN_ERROR_SQL); + throw new \Exception(__METHOD__." : unable to build sql query, parameter 'tables' is empty.", QN_ERROR_SQL); } /* irrelevant if(!empty($fields) && !is_array($fields)) throw new \Exception(__METHOD__." : unable to build sql query, parameter 'fields' is not an array.", QN_ERROR_SQL); @@ -519,14 +504,6 @@ public function setRecords($table, $ids, $fields, $conditions=null, $id_field='i return $this->sendQuery($sql); } - /** - * Inserts new records in specified table. - * - * @param string $table name of the table in which insert the records - * @param array $fields list of involved fields - * @param array $values array of arrays specifying the values related to each specified field - * @return resource reference to query resource - */ public function addRecords($table, $fields, $values) { if (!is_array($fields) || !is_array($values)) { throw new \Exception(__METHOD__.' : at least one parameter is missing', QN_ERROR_SQL); @@ -543,4 +520,18 @@ public function deleteRecords($table, $ids, $conditions=null, $id_field='id') { return $this->sendQuery($sql); } + /** + * Fetch and increment the column of a series of records in a single operation. + * + * MySQL requires multi queries to support a single input with instructions separator. + * So we use LOCK TABLES and UNLOCK TABLES to make sure no change occurs between update and read. + */ + public function incRecords($table, $ids, $field, $increment, $id_field='id') { + $res = null; + $this->sendQuery('LOCK TABLES `'.$table.'` WRITE;'); + $this->sendQuery("UPDATE `{$table}` SET `{$field}` = `{$field}` + $increment WHERE `{$id_field}` in (".implode(',', $ids).");"); + $res = $this->sendQuery("SELECT `{$id_field}`, `{$field}` FROM `{$table}` WHERE `{$id_field}` in (".implode(',', $ids).");"); + $this->sendQuery('UNLOCK TABLES;'); + return $res; + } } diff --git a/lib/equal/db/DBManipulatorSQLite.class.php b/lib/equal/db/DBManipulatorSQLite.class.php index 8a0b32e44..0574cbf98 100644 --- a/lib/equal/db/DBManipulatorSQLite.class.php +++ b/lib/equal/db/DBManipulatorSQLite.class.php @@ -10,8 +10,7 @@ * DBManipulator implementation for MySQL server. * */ - -class DBManipulatorSQLite extends DBManipulator { +final class DBManipulatorSQLite extends DBManipulator { public static $types_associations = [ @@ -66,12 +65,14 @@ public function connect($auto_select=true) { // by convention the DB file is the given DB_NAME with `.db` suffix $db_file = QN_BASEDIR.'/bin/'.$this->db_name.'.db'; - $this->dbms_handler = new \SQLite3($db_file, SQLITE3_OPEN_READWRITE | SQLITE3_OPEN_CREATE, $this->password); - if(!file_exists($db_file)) { return false; } + $this->dbms_handler = new \SQLite3($db_file, SQLITE3_OPEN_READWRITE | SQLITE3_OPEN_CREATE, $this->password); + // make sure PHP process waits when an exclusive transaction is pending + $this->dbms_handler->busyTimeout(3000); + return $this; } @@ -417,20 +418,6 @@ private function getConditionClause($id_field, $ids, $conditions) { return $sql; } - /** - * Get records from specified table, according to some conditions. - * - * @param array $tables name of involved tables - * @param array $fields list of requested fields - * @param array $ids ids to which the selection is limited - * @param array $conditions list of arrays (field, operand, value) - * @param string $id_field name of the id field ('id' by default) - * @param mixed $order string holding name of the order field or maps holding field names as keys and sorting as value - * @param integer $start - * @param integer $limit - * - * @return resource reference to query resource - */ public function getRecords($tables, $fields=NULL, $ids=NULL, $conditions=NULL, $id_field='id', $order=[], $start=0, $limit=0) { // cast tables to an array (passing a single table is accepted) if(!is_array($tables)) { @@ -531,14 +518,6 @@ public function setRecords($table, $ids, $fields, $conditions=null, $id_field='i return $this->sendQuery($sql); } - /** - * Inserts new records in specified table. - * - * @param string $table name of the table in which insert the records - * @param array $fields list of involved fields - * @param array $values array of arrays specifying the values related to each specified field - * @return resource reference to query resource - */ public function addRecords($table, $fields, $values) { if (!is_array($fields) || !is_array($values)) { throw new \Exception(__METHOD__.' : at least one parameter is missing', QN_ERROR_SQL); @@ -555,4 +534,16 @@ public function deleteRecords($table, $ids, $conditions=null, $id_field='id') { return $this->sendQuery($sql); } + /** + * Fetch and increment the column of a series of records in a single operation. + * + */ + public function incRecords($table, $ids, $field, $increment, $id_field='id') { + $sql = 'BEGIN EXCLUSIVE TRANSACTION;'; + $sql .= "UPDATE `{$table}` SET `{$field}` = `{$field}` + $increment WHERE `{$id_field}` in (".implode(',', $ids).");"; + $sql .= 'COMMIT TRANSACTION;'; + $sql .= "SELECT `{$id_field}`, `{$field}` FROM `{$table}` WHERE `{$id_field}` in (".implode(',', $ids).");"; + return $this->sendQuery($sql); + } + } diff --git a/lib/equal/db/DBManipulatorSqlSrv.class.php b/lib/equal/db/DBManipulatorSqlSrv.class.php index 113d2c4f1..c859ec3cf 100644 --- a/lib/equal/db/DBManipulatorSqlSrv.class.php +++ b/lib/equal/db/DBManipulatorSqlSrv.class.php @@ -10,8 +10,7 @@ * DBManipulator implementation for MS SQL server. * */ - -class DBManipulatorSqlSrv extends DBManipulator { +final class DBManipulatorSqlSrv extends DBManipulator { /* @@ -289,7 +288,7 @@ function sendQuery($query, $sql_operation='') { foreach($this->members as $member) { $member->sendQuery($query); } - if($sql_operation =='insert') { + if($sql_operation == 'insert') { if($row = sqlsrv_fetch_array($result, SQLSRV_FETCH_ASSOC)) { $this->setLastId($row['id']); } @@ -330,13 +329,13 @@ private function escapeString($value) { if(gettype($value) == 'string' && strlen($value) == 0) { $result = "''"; } - else if(in_array(gettype($value), ['integer', 'double'])) { + elseif(in_array(gettype($value), ['integer', 'double'])) { $result = $value; } - else if(gettype($value) == 'boolean') { + elseif(gettype($value) == 'boolean') { $result = ($value)?'1':'0'; } - else if(is_null($value)) { + elseif(is_null($value)) { $result = 'NULL'; } else { @@ -426,20 +425,6 @@ private function getConditionClause($id_field, $ids, $conditions) { return $sql; } - /** - * Get records from specified table, according to some conditions. - * - * @param array $tables name of involved tables - * @param array $fields list of requested fields - * @param array $ids ids to which the selection is limited - * @param array $conditions list of arrays (field, operand, value) - * @param string $id_field name of the id field ('id' by default) - * @param mixed $order string holding name of the order field or maps holding field nmaes as keys and sorting as value - * @param integer $start - * @param integer $limit - * - * @return resource reference to query resource - */ public function getRecords($tables, $fields=NULL, $ids=NULL, $conditions=NULL, $id_field='id', $order=[], $start=0, $limit=0) { // cast tables to an array (passing a single table is accepted) $tables = (array) $tables; @@ -526,15 +511,6 @@ public function setRecords($table, $ids, $fields, $conditions=null, $id_field='i return $this->sendQuery($sql, 'update'); } - - /** - * Inserts new records in specified table. - * - * @param string $table name of the table in which insert the records - * @param array $fields list of involved fields - * @param array $values array of arrays specifying the values related to each specified field - * @return resource reference to query resource - */ public function addRecords($table, $fields, $values) { if (!is_array($fields) || !is_array($values)) { throw new \Exception(__METHOD__.' : at least one parameter is missing', QN_ERROR_SQL); @@ -551,4 +527,18 @@ public function deleteRecords($table, $ids, $conditions=null, $id_field='id') { return $this->sendQuery($sql, 'delete'); } + /** + * Fetch and increment the column of a series of records in a single operation. + * + * For unknown reason, if the select is done after the update, no result set is returned. + * That is why we compute the expected result in the first select statement, marked with TABLOCKX to make sure the server locks the table before updating it. + */ + public function incRecords($table, $ids, $field, $increment, $id_field='id') { + $sql = 'BEGIN TRANSACTION;'; + $sql .= "SELECT [{$id_field}], ([{$field}] + $increment) as $field FROM [{$table}] WITH (TABLOCKX) WHERE [{$id_field}] in (".implode(',', $ids).");"; + $sql .= "UPDATE [{$table}] SET [{$field}] = [{$field}] + $increment WHERE [{$id_field}] in (".implode(',', $ids).");"; + $sql .= 'COMMIT;'; + return $this->sendQuery($sql, 'update'); + } + } diff --git a/lib/equal/error/Reporter.class.php b/lib/equal/error/Reporter.class.php index a49f78567..629beb1c2 100644 --- a/lib/equal/error/Reporter.class.php +++ b/lib/equal/error/Reporter.class.php @@ -15,16 +15,25 @@ class Reporter extends Service { private $thread_id; + /** + * Static list of constants required by current provider + * + */ + public static function constants() { + return ['DEBUG_MODE', 'DEBUG_LEVEL', 'QN_LOG_STORAGE_DIR', 'QN_REPORT_SYSTEM', 'QN_REPORT_FATAL', 'QN_REPORT_ERROR', 'QN_REPORT_WARNING', 'QN_REPORT_DEBUG', 'QN_REPORT_INFO', 'QN_REPORT_DEBUG']; + } + /** * Constructor defines which methods have to be called when errors and uncaught exceptions occur * + * Note: $thread_id depends on the current PHP thread. + * A same thread can stack several contexts. In the console, logs are grouped based on their thread_id. */ public function __construct() { - // #memo - $thread_id depends on the current PHP thread. A same thread can stack several contexts. In the console, logs are grouped based on their thread_id. $this->thread_id = substr(md5(getmypid().';'.hrtime(true)), 0, 8); $this->debug_mode = (defined('DEBUG_MODE'))?constant('DEBUG_MODE'):0; $this->debug_level = (defined('DEBUG_LEVEL'))?constant('DEBUG_LEVEL'):0; - // ::errorHandler() will deal with error and debug messages depending on debug source value + // ::errorHandler() will deal with errors and debug messages depending on debug source value ini_set('display_errors', 1); // use QN_REPORT_x for reporting, E_ERROR for fatal errors only, E_ALL for all errors error_reporting($this->debug_level); @@ -33,16 +42,8 @@ public function __construct() { } /** - * Static list of constants required by current provider - * - */ - public static function constants() { - return ['QN_LOG_STORAGE_DIR', 'QN_REPORT_FATAL', 'QN_REPORT_ERROR', 'QN_REPORT_WARNING', 'QN_REPORT_DEBUG', 'QN_REPORT_INFO', 'QN_REPORT_DEBUG']; - } - - /** - * Handles uncaught exceptions, which include deliberately triggered fatal-error - * In all cases, these are critical errors that cannot be recovered and need an immediate stop (fatal error) + * Handles uncaught exceptions, which include deliberately triggered fatal-error. + * Uncaught errors imply a critical issue that cannot be recovered and need an immediate stop (fatal error). */ public static function uncaughtExceptionHandler($exception) { self::handleThrowable($exception); @@ -57,6 +58,8 @@ public static function handleThrowable($exception) { $backtrace = $exception->getTrace(); if(count($backtrace)) { $trace = array_shift($backtrace); + $trace['file'] = $exception->getFile(); + $trace['line'] = $exception->getLine(); $trace['stack'] = $backtrace; $instance->log(QN_REPORT_ERROR, $msg, $trace); } @@ -73,8 +76,8 @@ public static function handleThrowable($exception) { * @param mixed $errcontext */ public static function errorHandler($errno, $errmsg, $errfile='', $errline=0, $errcontext=[]) { - // dismiss handler if not required - if (!(error_reporting() & $errno)) { + // dismiss processing if not required + if ($errno > 0 && !(error_reporting() & $errno)) { return; } // adapt error code @@ -83,12 +86,12 @@ public static function errorHandler($errno, $errmsg, $errfile='', $errline=0, $e $depth = 0; switch($errno) { // handler was invoked using trigger_error() - case QN_REPORT_DEBUG: // E_USER_DEPRECATED - case QN_REPORT_INFO: // E_USER_NOTICE - case QN_REPORT_WARNING: // E_USER_WARNING - case QN_REPORT_ERROR: // E_USER_ERROR - // #memo - fatal errors always stop the script before reaching this point - case QN_REPORT_FATAL: // E_ERROR + case EQ_REPORT_DEBUG: // E_USER_DEPRECATED + case EQ_REPORT_INFO: // E_USER_NOTICE + case EQ_REPORT_WARNING: // E_USER_WARNING + case EQ_REPORT_ERROR: // E_USER_ERROR + case EQ_REPORT_FATAL: // E_ERROR + case EQ_REPORT_SYSTEM: // 0 $depth = 2; break; // handler was invoked by PHP internals @@ -103,10 +106,12 @@ public static function errorHandler($errno, $errmsg, $errfile='', $errline=0, $e case E_WARNING: case E_CORE_WARNING: case E_COMPILE_WARNING: + $code = QN_REPORT_WARNING; + break; case E_NOTICE: case E_STRICT: case E_DEPRECATED: - $code = QN_REPORT_WARNING; + $code = QN_REPORT_INFO; break; } // retrieve instance and log error @@ -119,18 +124,17 @@ public static function errorHandler($errno, $errmsg, $errfile='', $errline=0, $e * Appends one line to the log file. */ private function log($code, $msg, $trace) { - // discard non-applicable log requests - if($this->debug_mode == 0 || $this->debug_level == 0 || !($code & $this->debug_level)) { + // discard non-applicable log requests, with exception for $code = 0 (system message that must always be logged) + if($code > 0 && ($this->debug_mode == 0 || $this->debug_level == 0 || !($code & $this->debug_level))) { return; } - // check reporting mode, if provided - $mode = QN_MODE_PHP; + // retrieve reporting mode, if provided + $mode = EQ_MODE_PHP; if(strpos($msg, '::') == 3) { - // default to mask QN_MODE_PHP - $source = QN_MODE_PHP; + $source = $mode; $parts = explode('::', $msg, 2); if($parts && count($parts) > 1) { - $source = (strlen($parts[0]))?('QN_MODE_'.$parts[0]):$source; + $source = (strlen($parts[0]))?('EQ_MODE_'.$parts[0]):$source; $msg = $parts[1]; } if(!is_numeric($source) && @constant($source)) { @@ -139,8 +143,12 @@ private function log($code, $msg, $trace) { $mode = (int) $source; } // discard non-applicable log requests - if(!($this->debug_mode & $mode)) { - return; + // #memo - SYSTEM are always logged (code == 0) + if($code > 0) { + // discard if mode is not marked in debug_mode + if(!($this->debug_mode & $mode)) { + return; + } } $time_parts = explode(" ", microtime()); @@ -152,7 +160,7 @@ private function log($code, $msg, $trace) { 'level' => qn_debug_code_name($code), 'mode' => qn_debug_mode_name($mode), 'class' => (isset($trace['class']))?$trace['class']:'', - 'function' => (isset($trace['function']))?$trace['function']:'', + 'function' => (isset($trace['function']))?(strlen($trace['function'])?$trace['function'].'()':'[main]'):'', 'file' => (isset($trace['file']))?$trace['file']:'', 'line' => (isset($trace['line']))?$trace['line']:'', 'message' => $msg, @@ -223,24 +231,24 @@ private static function getTrace($offset=0) { } public function fatal($msg) { - $this->log(QN_REPORT_FATAL, $msg, self::getTrace(2)); + $this->log(QN_REPORT_FATAL, $msg, self::getTrace(1)); die('fatal_error'); } public function error($msg) { - $this->log(QN_REPORT_ERROR, $msg, self::getTrace(2)); + $this->log(QN_REPORT_ERROR, $msg, self::getTrace(1)); } public function warning($msg) { - $this->log(QN_REPORT_WARNING, $msg, self::getTrace(2)); + $this->log(QN_REPORT_WARNING, $msg, self::getTrace(1)); } public function info($msg) { - $this->log(QN_REPORT_INFO, $msg, self::getTrace(2)); + $this->log(QN_REPORT_INFO, $msg, self::getTrace(1)); } public function debug($msg) { - $this->log(QN_REPORT_DEBUG, $msg, self::getTrace(2)); + $this->log(QN_REPORT_DEBUG, $msg, self::getTrace(1)); } } diff --git a/lib/equal/http/HttpResponse.class.php b/lib/equal/http/HttpResponse.class.php index b318e572a..57f9b740c 100644 --- a/lib/equal/http/HttpResponse.class.php +++ b/lib/equal/http/HttpResponse.class.php @@ -99,7 +99,7 @@ public function send() { case 'application/vnd.api+json': case 'application/x-json': case 'application/json': - $body = json_encode($body, JSON_PRETTY_PRINT); + $body = json_encode($body, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); if($body === false) { throw new \Exception('invalid_json_input', QN_ERROR_UNKNOWN); } diff --git a/lib/equal/organic/Service.class.php b/lib/equal/organic/Service.class.php index 9b847e99f..8e2259e79 100644 --- a/lib/equal/organic/Service.class.php +++ b/lib/equal/organic/Service.class.php @@ -8,7 +8,7 @@ use equal\services\Container; class Service extends Singleton { - /* All services are instanciated throught the service container + /* All services are instantiated through the service container which instance is, in turn, made available as public member */ public $container; diff --git a/lib/equal/orm/Collection.class.php b/lib/equal/orm/Collection.class.php index dd58b14f0..a70971cf8 100644 --- a/lib/equal/orm/Collection.class.php +++ b/lib/equal/orm/Collection.class.php @@ -386,14 +386,14 @@ public function lang($lang) { * @throws Exception if some value could not be validated against class constraints (see {class}::getConstraints method) */ private function validate(array $fields, $ids=[], $check_unique=false, $check_required=false) { - $validation = $this->orm->validate($this->class, $ids, $fields, $check_unique, $check_required); - if($validation < 0 || count($validation)) { - foreach($validation as $error_code => $error_descr) { - if(is_array($error_descr)) { - $error_descr = serialize($error_descr); + $errors = $this->orm->validate($this->class, $ids, $fields, $check_unique, $check_required); + if(count($errors)) { + foreach($errors as $error_id => $description) { + if(is_array($description)) { + $description = serialize($description); } // send error using the same format as the announce method - throw new \Exception($error_descr, (int) $error_code); + throw new \Exception($description, (int) $error_id); } } } @@ -665,10 +665,10 @@ public function read($fields, $lang=null) { $requested_fields = []; // 'id': we might access an object directly by giving its `id`. - // 'state': the state of the object is provided for concurrency control (check that a draft object is not validated twice). - // 'deleted': since some objects might have been soft-deleted we need to load the `deleted` state in order to know if object needs to be in the result set or not. - // 'modified': the last update timestamp is always provided. At update, if modified is provided, it is compared to the current timestamp to detect concurrent changes. - // #memo - we cannot add 'name' by default since it might be a computed field (that could lead to a circular dependency) + // 'state': the state of the object is provided for concurrency control (check that a draft object is not instantiated twice). + // 'deleted': since some objects might have been soft-deleted, we need to load the `deleted` state in order to know if object needs to be in the result set or not. + // 'modified': the last update timestamp is always provided. At update, if `modified` is provided, it is compared to the current timestamp to detect concurrent changes. + // #memo - we cannot add 'name' by default since it might be (an alias to) a computed field (that could lead to a circular dependency) $mandatory_fields = ['id', /*'name',*/ 'state', 'deleted', 'modified']; foreach($fields as $key => $val ) { @@ -737,16 +737,19 @@ public function read($fields, $lang=null) { if(is_numeric($field)) { continue; } - // we accept single value as subfields + // accept both array or single value as subfields $subfields = (array) $subfields; // #memo - using Field object guarantees support for `alias` and `computed` fields $targetField = $this->model->getField($field); + if(!$targetField) { + continue; + } $target = $targetField->getDescriptor(); - $target_type = (isset($target['result_type']))?$target['result_type']:$target['type']; - if(!in_array($target_type, ['one2many', 'many2one', 'many2many'])) { + if(!in_array($target['result_type'], ['one2many', 'many2one', 'many2many'])) { continue; } + $children_ids = []; foreach($this->objects as $object) { foreach((array) $object[$field] as $oid) { @@ -763,8 +766,8 @@ public function read($fields, $lang=null) { foreach($this->objects as $id => $object) { /** @var Collection */ $children = $target['foreign_object']::ids($this->objects[$id][$field])->read($subfields, ($lang)?$lang:$this->lang); - if($target_type == 'many2one') { - // #memo - result might be null or an Object (that might contain sub-collections) + 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(); } else { diff --git a/lib/equal/orm/Field.class.php b/lib/equal/orm/Field.class.php index ef1d930d7..94dbb3b58 100644 --- a/lib/equal/orm/Field.class.php +++ b/lib/equal/orm/Field.class.php @@ -12,7 +12,9 @@ class Field { /** - * Descriptor of the field, as returned by Model::getColumns() + * Descriptor of the field. + * In addition to properties from `Model::getColumns()`, `Field::descriptor` always as a `result_type` property. + * * @var array */ private $descriptor = []; @@ -30,11 +32,14 @@ class Field { * @param array $descriptor Associative array mapping field properties and their values. */ public function __construct(array $descriptor) { - // store original descriptor - $this->descriptor = $descriptor; if(isset($descriptor['type'])) { $this->type = $descriptor['type']; } + $this->descriptor = $descriptor; + // ensure local descriptor always has a result_type property + if(!isset($descriptor['result_type'])) { + $this->descriptor['result_type'] = $this->type; + } } /** @@ -82,7 +87,35 @@ public function getUsage(): Usage { * @return array */ public function getConstraints(): array { - return $this->getUsage()->getConstraints(); + // generate constraint based on type + $result_type = $this->descriptor['result_type']; + + $constraints = [ + 'invalid_type' => [ + 'message' => "Value is not of type {$result_type}.", + 'function' => function($value) use($result_type) { + static $map = [ + 'bool' => 'boolean', + 'int' => 'integer', + 'float' => 'double', + 'text' => 'string', + 'date' => 'integer', + 'datetime' => 'integer', + 'file' => 'string', + 'binary' => 'string', + 'many2one' => 'integer', + 'one2many' => 'array', + 'many2many' => 'array' + ]; + // fix types to match values returned by PHP `gettype()` + $mapped_type = $map[$result_type] ?? $result_type; + return (gettype($value) == $mapped_type); + } + ] + ]; + + // append constraints based on usage + return array_merge($constraints, $this->getUsage()->getConstraints()); } public function getDescriptor(): array { diff --git a/lib/equal/orm/ObjectManager.class.php b/lib/equal/orm/ObjectManager.class.php index 73b38b892..2888c315f 100644 --- a/lib/equal/orm/ObjectManager.class.php +++ b/lib/equal/orm/ObjectManager.class.php @@ -7,7 +7,7 @@ namespace equal\orm; use equal\organic\Service; -use equal\db\DBConnection; +use equal\db\DBConnector; use equal\data\DataValidator; use \Exception as Exception; @@ -62,8 +62,8 @@ class ObjectManager extends Service { private $last_error; /** - * Instance to a DBConnection object - * @var DBConnection + * DB connector instance + * @var DBConnector */ private $db; @@ -71,22 +71,25 @@ class ObjectManager extends Service { public static $simple_types = array('boolean', 'integer', 'float', 'string', 'text', 'date', 'time', 'datetime', 'file', 'binary', 'many2one'); public static $complex_types = array('one2many', 'many2many', 'computed'); + /** + * - dev-2.0 - 'dependencies' has been deprecated in favor to 'dependents' + */ public static $valid_attributes = [ 'alias' => array('description', 'help', 'type', 'visible', 'default', 'usage', 'alias', 'required', 'deprecated'), - 'boolean' => array('description', 'help', 'type', 'visible', 'default', 'dependencies', 'readonly', 'usage', 'required', 'deprecated', 'onupdate'), - 'integer' => array('description', 'help', 'type', 'visible', 'default', 'dependencies', 'readonly', 'usage', 'required', 'deprecated', 'onupdate', 'selection', 'unique'), - 'float' => array('description', 'help', 'type', 'visible', 'default', 'dependencies', 'readonly', 'usage', 'required', 'deprecated', 'onupdate', 'selection', 'precision'), - 'string' => array('description', 'help', 'type', 'visible', 'default', 'dependencies', 'readonly', 'usage', 'required', 'deprecated', 'onupdate', 'multilang', 'selection', 'unique'), - 'text' => array('description', 'help', 'type', 'visible', 'default', 'dependencies', 'readonly', 'usage', 'required', 'deprecated', 'onupdate', 'multilang'), - 'date' => array('description', 'help', 'type', 'visible', 'default', 'dependencies', 'readonly', 'usage', 'required', 'deprecated', 'onupdate'), - 'time' => array('description', 'help', 'type', 'visible', 'default', 'dependencies', 'readonly', 'usage', 'required', 'deprecated', 'onupdate'), - 'datetime' => array('description', 'help', 'type', 'visible', 'default', 'dependencies', 'readonly', 'usage', 'required', 'deprecated', 'onupdate'), - 'file' => array('description', 'help', 'type', 'visible', 'default', 'dependencies', 'readonly', 'usage', 'required', 'deprecated', 'onupdate', 'multilang'), - 'binary' => array('description', 'help', 'type', 'visible', 'default', 'dependencies', 'readonly', 'usage', 'required', 'deprecated', 'onupdate', 'multilang'), - 'many2one' => array('description', 'help', 'type', 'visible', 'default', 'dependencies', 'readonly', 'required', 'deprecated', 'foreign_object', 'domain', 'onupdate', 'ondelete', 'multilang'), - 'one2many' => array('description', 'help', 'type', 'visible', 'default', 'dependencies', 'readonly', 'deprecated', 'foreign_object', 'foreign_field', 'domain', 'onupdate', 'ondetach', 'order', 'sort'), - 'many2many' => array('description', 'help', 'type', 'visible', 'default', 'dependencies', 'readonly', 'deprecated', 'foreign_object', 'foreign_field', 'rel_table', 'rel_local_key', 'rel_foreign_key', 'domain', 'onupdate'), - 'computed' => array('description', 'help', 'type', 'visible', 'default', 'dependencies', 'readonly', 'deprecated', 'result_type', 'usage', 'function', 'onupdate', 'onrevert', 'store', 'instant', 'multilang', 'selection', 'foreign_object') + 'boolean' => array('description', 'help', 'type', 'visible', 'default', 'dependencies', 'dependents', 'readonly', 'usage', 'required', 'deprecated', 'onupdate'), + 'integer' => array('description', 'help', 'type', 'visible', 'default', 'dependencies', 'dependents', 'readonly', 'usage', 'required', 'deprecated', 'onupdate', 'selection', 'unique'), + 'float' => array('description', 'help', 'type', 'visible', 'default', 'dependencies', 'dependents', 'readonly', 'usage', 'required', 'deprecated', 'onupdate', 'selection', 'precision'), + 'string' => array('description', 'help', 'type', 'visible', 'default', 'dependencies', 'dependents', 'readonly', 'usage', 'required', 'deprecated', 'onupdate', 'multilang', 'selection', 'unique'), + 'text' => array('description', 'help', 'type', 'visible', 'default', 'dependencies', 'dependents', 'readonly', 'usage', 'required', 'deprecated', 'onupdate', 'multilang'), + 'date' => array('description', 'help', 'type', 'visible', 'default', 'dependencies', 'dependents', 'readonly', 'usage', 'required', 'deprecated', 'onupdate'), + 'time' => array('description', 'help', 'type', 'visible', 'default', 'dependencies', 'dependents', 'readonly', 'usage', 'required', 'deprecated', 'onupdate'), + 'datetime' => array('description', 'help', 'type', 'visible', 'default', 'dependencies', 'dependents', 'readonly', 'usage', 'required', 'deprecated', 'onupdate'), + 'file' => array('description', 'help', 'type', 'visible', 'default', 'dependencies', 'dependents', 'readonly', 'usage', 'required', 'deprecated', 'onupdate', 'multilang'), + 'binary' => array('description', 'help', 'type', 'visible', 'default', 'dependencies', 'dependents', 'readonly', 'usage', 'required', 'deprecated', 'onupdate', 'multilang'), + 'many2one' => array('description', 'help', 'type', 'visible', 'default', 'dependencies', 'dependents', 'readonly', 'required', 'deprecated', 'foreign_object', 'domain', 'onupdate', 'ondelete', 'multilang'), + 'one2many' => array('description', 'help', 'type', 'visible', 'default', 'dependencies', 'dependents', 'readonly', 'deprecated', 'foreign_object', 'foreign_field', 'domain', 'onupdate', 'ondetach', 'order', 'sort'), + 'many2many' => array('description', 'help', 'type', 'visible', 'default', 'dependencies', 'dependents', 'readonly', 'deprecated', 'foreign_object', 'foreign_field', 'rel_table', 'rel_local_key', 'rel_foreign_key', 'domain', 'onupdate'), + 'computed' => array('description', 'help', 'type', 'visible', 'default', 'dependencies', 'dependents', 'readonly', 'deprecated', 'result_type', 'usage', 'function', 'onupdate', 'onrevert', 'store', 'instant', 'multilang', 'selection', 'foreign_object') ]; public static $mandatory_attributes = [ @@ -170,9 +173,9 @@ class ObjectManager extends Service { ]; /** - * @param DBConnection $db Instance of the Service allowing connection to DBMS (connection might not be established yet). + * @param DBConnector $db Instance of the Service allowing connection to DBMS (connection might not be established yet). */ - protected function __construct(DBConnection $db) { + protected function __construct(DBConnector $db) { $this->db = &$db; $this->packages = null; $this->cache = []; @@ -197,7 +200,7 @@ public static function constants() { } /** - * #todo - deprecate : use the DBConnection service instead + * #todo - deprecate : use the DBConnector instead * @deprecated */ public function getDB() { @@ -205,10 +208,10 @@ public function getDB() { } /** - * Provide the db handler (DBConnection instance). + * Provide the db handler (DBConnector instance). * If the connection hasn't been established yet, tries to connect to DBMS. * - * @return DBConnection + * @return DBConnector */ private function getDBHandler() { // open DB connection, if not connected yet @@ -661,17 +664,26 @@ 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); } - $res = $this->callonce($class, $schema[$field]['function'], $ids, [], $lang, ['ids', 'lang']); - if($res > 0) { - foreach($ids as $oid) { - if(isset($res[$oid])) { - // #memo - do not adapt : we're dealing with PHP not SQL - $value = $res[$oid]; - } - else { - $value = null; + // spot the fields that have not been computed yet (or unset) during this cycle + $missing_ids = []; + foreach($ids as $oid) { + if(!isset($om->cache[$table_name][$oid][$lang][$field])) { + $missing_ids[] = $oid; + } + } + if(count($missing_ids)) { + $res = $this->callonce($class, $schema[$field]['function'], $missing_ids, [], $lang, ['ids', 'lang']); + if($res > 0) { + foreach($missing_ids as $oid) { + if(isset($res[$oid])) { + // #memo - do not adapt : we're dealing with PHP not SQL + $value = $res[$oid]; + } + else { + $value = null; + } + $om->cache[$table_name][$oid][$lang][$field] = $value; } - $om->cache[$table_name][$oid][$lang][$field] = $value; } } } @@ -710,7 +722,9 @@ private function load($class, $ids, $fields, $lang) { $fields_lists['simple'][] = $field; } } - else $fields_lists[$type][] = $field; + else { + $fields_lists[$type][] = $field; + } } // 2) load fields values, grouping fields by type @@ -862,7 +876,7 @@ private function store($class, $ids, $fields, $lang) { $value = $om->cache[$table_name][$oid][$lang][$field]; if(!is_array($value)) { if(is_numeric($value)) { - $value = [intval($value)]; + $value = (array) intval($value); } else { trigger_error("ORM::wrong value for field '$field' of class '$class', should be an array", QN_REPORT_ERROR); @@ -896,14 +910,14 @@ private function store($class, $ids, $fields, $lang) { break; case 'null': default: - $om->db->setRecords($foreign_table, $ids_to_remove, [ $schema[$field]['foreign_field'] => 0 ] ); + $om->db->setRecords($foreign_table, $ids_to_remove, [ $schema[$field]['foreign_field'] => null ] ); break; } } } else { // remove relation by setting pointing id to 0 - $om->db->setRecords($foreign_table, $ids_to_remove, [$schema[$field]['foreign_field'] => 0]); + $om->db->setRecords($foreign_table, $ids_to_remove, [$schema[$field]['foreign_field'] => null]); } } // add relation by setting the pointing id (overwrite previous value if any) @@ -1027,6 +1041,10 @@ private function store($class, $ids, $fields, $lang) { */ public function callonce($class, $method, $ids, $values=[], $lang=null, $signature=['ids', 'values', 'lang']) { trigger_error("ORM::calling orm\ObjectManager::callonce {$class}::{$method}", QN_REPORT_DEBUG); + + // stack current state of object_methods map (current state is restored at the end of the method) + $object_methods_state = $this->object_methods; + $result = []; $lang = ($lang)?$lang:constant('DEFAULT_LANG'); @@ -1056,7 +1074,7 @@ public function callonce($class, $method, $ids, $values=[], $lang=null, $signatu $this->object_methods[$called_class][$called_method] = []; } - // prevent inner loops and several calls to same handler with identical ids during the cycle (subsequent update() calls) + // prevent inner loops (several calls to same handler with identical ids) during the cycle (subsequent `callonce()` calls) $processed_ids = $this->object_methods[$called_class][$called_method]; $unprocessed_ids = array_diff((array) $ids, $processed_ids); $this->object_methods[$called_class][$called_method] = array_merge($processed_ids, $unprocessed_ids); @@ -1101,6 +1119,9 @@ public function callonce($class, $method, $ids, $values=[], $lang=null, $signatu $result = $e->getCode(); } + // unstack global object_methods state + $this->object_methods = $object_methods_state; + return $result; } @@ -1179,6 +1200,7 @@ public function call($class, $method, $ids, $values=[], $lang=null, $signature=[ /** * Retrieve the static instance of a given class (Model with default values). + * This method is registered as autoload handler in `eq.lib.php`. * * @return boolean|Object Returns the static instance of the model with default values. If no Model matches the class name returns false. */ @@ -1189,8 +1211,7 @@ public function getModel($class) { } catch(Exception $e) { trigger_error($e->getMessage(), QN_REPORT_ERROR); - // #todo - validate (this is the only public method in ORM that raises an Exception) - throw new Exception("unknown class '{$class}'", QN_ERROR_UNKNOWN_OBJECT); + // #memo - another autoload handler might be registered, so we relay without raising an exception } return $model; } @@ -1202,6 +1223,7 @@ public function getLastError() { /** * Checks whether a set of values is valid according to given class definition. * This is done using the class validation method. + * Relations consistency check (verifying that targeted object(s) actually exist) is not performed here. * * Result example: * "INVALID_PARAM": { @@ -1213,16 +1235,16 @@ public function getLastError() { * @param string $class Entity name. * @param array $ids Array of objects identifiers. * @param array $values Associative array mapping fields names with values to be assigned to the object(s). - * @param boolean $check_unique Request check for unicity constraints (related to getUnique method). + * @param boolean $check_unique Request check for unique constraints (related to getUnique method). * @param boolean $check_required Request check for required fields (and _self constraints). + * * @return array 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 validate($class, $ids, $values, $check_unique=false, $check_required=false) { - // #todo : check relational fields consistency (make sure that target object(s) actually exist) - $res = []; + /** @var \equal\orm\Model */ $model = $this->getStaticInstance($class); $schema = $model->getSchema(); @@ -1248,109 +1270,54 @@ public function validate($class, $ids, $values, $check_unique=false, $check_requ * 2) MODEL constraint check */ - - // // #todo - // // check constraints implied by type and usage - // foreach($values as $field => $value) { - // /** @var \equal\orm\Field */ - // $f = $model->getField($class); - // $usage = $f->getUsage(); - // $constraints = $f->getConstraints(); - // foreach($constraints as $error_id => $constraint) { - // if(!isset($constraint['function'])) { - // continue; - // } - // $fn = $constraint['function']; - // if(is_callable($fn)) { - // $fn->bindTo($usage); - // if(!call_user_func($fn, $value)) { - // $res[$field][$error_id] = $constraint['message']; - // } - // } - // } - // } - - // // + support $model->getConstraints() - - // get constraints defined in the model (schema) $constraints = $model->getConstraints(); - + // append constraints implied by type and usage foreach($values as $field => $value) { - // add constraints based on field type : check that given value is not bigger than related DBMS column capacity - if(isset($schema[$field]['type'])) { - $type = $schema[$field]['type']; - // adapt type based on usage - if(isset($schema[$field]['usage'])) { - switch($schema[$field]['usage']) { - // #todo - continue this list - case 'text': - case 'text/plain': - case 'text/json': - case 'text/html': - case 'text/json': - $type = 'text'; - break; - } + $f = $model->getField($field); + foreach($f->getConstraints() as $error_id => $constraint) { + if(!isset($constraint['function'])) { + continue; } - switch($type) { - case 'string': - $constraints[$field]['size_overflow'] = [ - 'message' => 'String length must be maximum 255 chars.', - 'function' => function($val, $obj) {return (strlen($val) <= 255);} - ]; - break; - case 'text': - $constraints[$field]['size_overflow'] = [ - 'message' => 'String length must be maximum 65,535 chars.', - 'function' => function($val, $obj) {return (strlen($val) <= 65535);} - ]; - break; - case 'file': - case 'binary': - $constraints[$field]['size_overflow'] = [ - 'message' => 'String length must be maximum '.constant('UPLOAD_MAX_FILE_SIZE').' chars.', - 'function' => function($val, $obj) {return (strlen($val) <= constant('UPLOAD_MAX_FILE_SIZE'));} + if(is_callable($constraint['function'])) { + $closure = \Closure::fromCallable($constraint['function']); + $closure->bindTo($f->getUsage()); + if(!isset($constraints[$field])) { + $constraints[$field] = []; + } + $constraints[$field][$error_id] = [ + 'message' => $constraint['message'], + 'function' => $closure ]; - break; } } - // add constraints based on `usage` attribute - if(isset($schema[$field]['usage']) && !empty($value)) { - $constraint = DataValidator::getConstraintFromUsage($schema[$field]['usage']); - $constraints[$field]['type_misuse'] = [ - 'message' => "Value [$value] does not comply with usage '{$schema[$field]['usage']}'.", - 'function' => $constraint['rule'] - ]; - } - // add constraints based on `required` attribute - /* - // #memo - has no effect - if(isset($schema[$field]['required']) && $schema[$field]['required']) { - $constraints[$field]['missing_mandatory'] = [ - 'message' => 'Missing mandatory value.', - 'function' => function($a) { return (isset($a) && (!is_string($a) || !empty($a))); } - ]; - } - */ - // check constraints - if(isset($constraints[$field])) { - foreach($constraints[$field] as $error_id => $constraint) { - if(isset($constraint['function']) ) { - $validation_func = $constraint['function']; - // #todo - use a single arg (validation should be independent from context, otherwise use cancreate/canupdate) - if(is_callable($validation_func) && !call_user_func($validation_func, $value, $values)) { - if(!isset($constraint['message'])) { - $constraint['message'] = 'Invalid field.'; - } - trigger_error("ORM::given value for field `{$field}` violates constraint : {$constraint['message']}", QN_REPORT_DEBUG); - $error_code = QN_ERROR_INVALID_PARAM; - if(!isset($res[$field])) { - $res[$field] = []; - } - $res[$field][$error_id] = $constraint['message']; - } + } + // check constraints + foreach($values as $field => $value) { + if(!isset($constraints[$field]) || empty($constraints[$field])) { + // ignore fields with no constraints + continue; + } + if($value === null) { + // all fields can be reset to null + continue; + } + foreach($constraints[$field] as $error_id => $constraint) { + if(!isset($constraint['function']) ) { + continue; + } + $validation_func = $constraint['function']; + // #todo - use a single arg (validation should be independent from context, otherwise use cancreate/canupdate) + if(is_callable($validation_func) && !call_user_func($validation_func, $value, $values)) { + if(!isset($constraint['message'])) { + $constraint['message'] = 'Invalid field.'; } + trigger_error("ORM::given value for field `{$field}` violates constraint : {$constraint['message']}", QN_REPORT_INFO); + $error_code = QN_ERROR_INVALID_PARAM; + if(!isset($res[$field])) { + $res[$field] = []; + } + $res[$field][$error_id] = $constraint['message']; } } } @@ -1396,7 +1363,7 @@ public function validate($class, $ids, $values, $check_unique=false, $check_requ elseif(isset($extra_values[$id][$field])) { $value = $extra_values[$id][$field]; } - else { + elseif(!isset($schema[$field])) { // ignore non-exiting fields continue; } @@ -1540,7 +1507,6 @@ public function create($class, $fields=null, $lang=null, $use_draft=true) { /** @var \equal\data\adapt\DataAdapterProvider */ $dap = $this->container->get('adapt'); foreach($creation_array as $field => $value) { - /** @var \equal\data\adapt\DataAdapter */ $adapter = $dap->get('sql'); $f = new Field($schema[$field]); // adapt value to SQL @@ -1641,8 +1607,6 @@ public function update($class, $ids=null, $fields=null, $lang=null, $create=fals }, ARRAY_FILTER_USE_KEY ); - // stack current state of object_methods map (we'll restore current state at the end of the update cycle) - $object_methods_state = $this->object_methods; // 3) make sure objects in the collection can be updated @@ -1682,6 +1646,7 @@ public function update($class, $ids=null, $fields=null, $lang=null, $create=fals // remember callbacks that are triggered by the update $onupdate_fields = []; $onrevert_fields = []; + $instant_fields = []; // update internal buffer with given values foreach($ids as $oid) { @@ -1699,8 +1664,13 @@ public function update($class, $ids=null, $fields=null, $lang=null, $create=fals $onupdate_fields[] = $field; } } - elseif(isset($schema[$field]['onrevert'])) { - $onrevert_fields[] = $field; + else { + if(isset($schema[$field]['onrevert'])) { + $onrevert_fields[] = $field; + } + if(isset($schema[$field]['instant']) && $schema[$field]['instant']) { + $instant_fields[$field] = true; + } } } // assign cache to object values @@ -1738,34 +1708,71 @@ public function update($class, $ids=null, $fields=null, $lang=null, $create=fals } } - // unstack global object_methods state - $this->object_methods = $object_methods_state; - - // 8) handle the resetting of the dependent computed fields - $dependencies = []; + $dependents = [ + 'primary' => [], + 'related' => [] + ]; foreach($fields as $field => $value) { // remember fields whose modification triggers resetting computed fields + // #todo - deprecate dependencies : use dependents if(isset($schema[$field]['dependencies'])) { - $dependencies = array_merge($dependencies, (array) $schema[$field]['dependencies']); + foreach((array) $schema[$field]['dependencies'] as $dependent) { + $dependents['primary'][$dependent] = true; + } + } + + if(isset($schema[$field]['dependents'])) { + foreach((array) $schema[$field]['dependents'] as $key => $val) { + // handle array notation + if(!is_numeric($key)) { + if(!isset($dependents['related'][$key])) { + $dependents['related'][$key] = []; + } + $dependents['related'][$key] = array_merge($dependents['related'][$key], (array) $val); + } + else { + $dependents['primary'][$val] = true; + } + } } } - // remember fields that must be re-computed instantly - $instant_fields = []; - foreach(array_unique($dependencies) as $dependency) { - // #todo - add support for dot notation - if(isset($schema[$dependency]) && $schema[$dependency]['type'] == 'computed') { - if(isset($schema[$dependency]['instant']) && $schema[$dependency]['instant']) { - $instant_fields[] = $dependency; + + // read all target fields at once + $this->load($class, $ids, array_keys($dependents['related']), $lang); + + foreach($dependents['related'] as $field => $subfields) { + $values = []; + foreach($subfields as $subfield) { + $values[$subfield] = null; + } + foreach($ids as $oid) { + $target_ids = (array) $this->cache[$table_name][$oid][$lang][$field]; + // allow cascade update (circular dependencies are checked in `core_test_package`) + $this->update($schema[$field]['foreign_object'], $target_ids, $values, $lang); + } + } + + // handle fields that must be re-computed instantly + $values = []; + foreach(array_keys($dependents['primary']) as $field) { + if(isset($schema[$field]) && $schema[$field]['type'] == 'computed') { + if(isset($schema[$field]['instant']) && $schema[$field]['instant']) { + $instant_fields[$field] = true; } - // allow cascade update - $this->update($class, $ids, [$dependency => null], $lang, $create); + $values[$field] = null; } } + + if(count($values)) { + // allow cascade update (circular dependencies are checked in `core_test_package`) + $this->update($class, $ids, $values, $lang); + } + if(count($instant_fields)) { - // re-compute 'instant' computed field - $this->read($class, $ids, array_unique($instant_fields), $lang); + // re-compute local 'instant' computed field + $this->load($class, $ids, array_keys($instant_fields), $lang); } @@ -1836,6 +1843,7 @@ public function read($class, $ids=null, $fields=null, $lang=null) { } // get static instance (check that given class exists) + /** @var \equal\orm\Model */ $model = $this->getStaticInstance($class); $schema = $model->getSchema(); // retrieve name of the DB table associated with the class @@ -1868,11 +1876,11 @@ public function read($class, $ids=null, $fields=null, $lang=null) { // handle fields with 'dot' notation if(strpos($field, '.') > 0) { $dot_fields[] = $field; - // drop dot fields (they will be handled later on) + // drop dot fields (they are handled in dedicated step) unset($fields[$key]); } // invalid field - else if(!isset($schema[$field])) { + elseif(!isset($schema[$field])) { // drop invalid fields unset($fields[$key]); trigger_error("ORM::unknown field '$field' for class : '$class'", QN_REPORT_WARNING); @@ -1941,42 +1949,55 @@ public function read($class, $ids=null, $fields=null, $lang=null) { // 5) handle dot fields - foreach($dot_fields as $field) { + // create a map associating fields with all subfields + $map_dot_fields = []; + foreach($dot_fields as $path) { // extract sub field and remainder - $parts = explode('.', $field, 2); + $parts = explode('.', $path, 2); // left side of the first dot - $path_field = $parts[0]; - // #todo - use getField - // retrieve final type of targeted field - $field_type = $this->getFinalType($class, $path_field); - // ignore non-relational fields - if(!$field_type || !in_array($field_type, ['many2one', 'one2many', 'many2many'])) { - continue; + $field = $parts[0]; + if(!isset($map_dot_fields[$field])) { + $map_dot_fields[$field] = []; } - // read the field values - $values = $this->read($class, $ids, (array) $path_field, $lang); + $map_dot_fields[$field][] = $parts[1]; + } - if($values < 0) { - continue; - } + // read all dot fields at once + $this->load($class, $ids, array_keys($map_dot_fields), $lang); - // recursively read sub objects - foreach($ids as $oid) { - // #memo - unreachable values are always set to null - $res[$oid][$field] = null; - if(isset($values[$oid][$path_field]) && isset($schema[$path_field]['foreign_object'])) { - $sub_class = $schema[$path_field]['foreign_object']; - $sub_ids = $values[$oid][$path_field]; - $sub_values = $this->read($sub_class, $sub_ids, (array) $parts[1], $lang); - if($sub_values > 0) { + // recursively read sub objects + foreach($map_dot_fields as $field => $sub_fields) { + foreach($sub_fields as $sub_path) { + // retrieve final type of targeted field + $f = $model->getField($field); + $descriptor = $f->getDescriptor(); + if(!$descriptor || !isset($descriptor['result_type']) || !isset($descriptor['foreign_object'])) { + // ignore invalid descriptors + continue; + } + $field_type = $descriptor['result_type']; + if(!$field_type || !in_array($field_type, ['many2one', 'one2many', 'many2many'])) { + // ignore non-relational fields + continue; + } + // recursively read sub objects for each object + // #todo - this could be improved by loading all targeted objects for current collection at once + foreach($ids as $oid) { + // #memo - for unreachable values, m2o are always set to null, and m2m or o2m to an empty array + $sub_ids = $this->cache[$table_name][$oid][$lang][$field]; + if(isset($sub_ids) || count($sub_ids)) { + $sub_values = $this->read($descriptor['foreign_object'], (array) $sub_ids, (array) $sub_path, $lang); + if($sub_values <= 0) { + continue; + } if($field_type == 'many2one') { $odata = reset($sub_values); if(is_array($odata) || is_a($odata, Model::getType())) { - $res[$oid][$field] = $odata[$parts[1]]; + $res[$oid][$field.'.'.$sub_path] = $odata[$sub_path]; } } else { - $res[$oid][$field] = $sub_values; + $res[$oid][$field.'.'.$sub_path] = $sub_values; } } } @@ -2174,7 +2195,7 @@ public function clone($class, $id, $values=[], $lang=null, $parent_field='') { $original = $res_r[$id]; $new_values = []; - // unset relations + id and parent_field (needs to be updated + could be part of unique contrainst) + // unset relations + id and parent_field (needs to be updated + could be part of unique constraint) foreach($original as $field => $value) { $def = $schema[$field]; if(!in_array($def['type'], ['one2many', 'many2many']) && !in_array($field, ['id', $parent_field])) { @@ -2233,6 +2254,50 @@ public function clone($class, $id, $values=[], $lang=null, $parent_field='') { return $res; } + /** + * Increment the field of an object by a given increment (integer value). + * + * note : We use this nomenclature because it has a recognized semantics and equivalence in many programming languages. + * + * @param string $class Class name of the object to clone. + * @param array $ids Array of ids of the objects to update. + * @param string $field Name of the field to increment (must have a numeric type). + * @param integer $increment Value by witch increment the field (positive or negative). + * + * @return int|array Returns an array of updated ids, or an error identifier in case an error occurred. + */ + public function fetchAndAdd($class, $ids, $field, $increment) { + $result = []; + $db = $this->getDBHandler(); + try { + // get static instance (checks that given class exists) + $object = $this->getStaticInstance($class); + // retrieve schema + $schema = $object->getSchema(); + // make sure that the targeted field exists and has a numeric type + if(!isset($schema[$field])) { + throw new Exception('unknown_field', EQ_ERROR_INVALID_PARAM); + } + if(!in_array($schema[$field]['type'], ['integer', 'float'])) { + throw new Exception('non_numeric_field', EQ_ERROR_INVALID_PARAM); + } + $table_name = $this->getObjectTableName($class); + // increment the field as an atomic operation + $res = $db->incRecords($table_name, $ids, $field, $increment); + // #todo - returned values must be similar to a + while ($row = $db->fetchArray($res)) { + // maintain ids order provided by the SQL sort + $result[$row['id']] = $row[$field]; + } + } + catch(Exception $e) { + trigger_error("ORM::".$e->getMessage(), QN_REPORT_WARNING); + $this->last_error = $e->getMessage(); + $result = $e->getCode(); + } + return $result; + } + /** * Returns applicable transitions based on a list of updated fields, according to the dependencies defined in the related workflow descriptors. * If no workflow is defined for the given class, an empty array is returned. @@ -2416,7 +2481,7 @@ public function search($class, $domain=null, $sort=['id' => 'asc'], $start='0', $table_name = $this->getObjectTableName($class); - // we use a nested closure to define a function that stores original table names and returns corresponding aliases + // use nested closure to store original table names and return corresponding aliases $add_table = function ($table_name) use (&$tables) { if(in_array($table_name, $tables)) { return array_search($table_name, $tables); @@ -2644,11 +2709,15 @@ public function search($class, $domain=null, $sort=['id' => 'asc'], $start='0', if($schema[$sort_field]['type'] == 'many2one' || (isset($schema[$sort_field]['result_type']) && $schema[$sort_field]['result_type'] == 'many2one') ) { $related_table = $this->getObjectTableName($schema[$sort_field]['foreign_object']); $related_table_alias = $add_table($related_table); - $select_fields[] = $related_table_alias.'.name'; + $related_schema = $this->getObjectSchema($schema[$sort_field]['foreign_object']); // #todo - shouldn't this condition be added to all clauses? $conditions[0][] = array($table_alias.'.'.$sort_field, '=', '`'.$related_table_alias.'.id'.'`'); $order_table_alias = $related_table_alias; $sort_field = 'name'; + while($related_schema[$sort_field]['type'] == 'alias') { + $sort_field = $related_schema[$sort_field]['alias']; + } + $select_fields[] = $related_table_alias.'.'.$sort_field; } else { $select_fields[] = $table_alias.'.'.$sort_field; diff --git a/lib/equal/orm/UsageFactory.class.php b/lib/equal/orm/UsageFactory.class.php index 1cbb06a1a..960faf67d 100644 --- a/lib/equal/orm/UsageFactory.class.php +++ b/lib/equal/orm/UsageFactory.class.php @@ -77,10 +77,13 @@ public static function create(string $usage): Usage { // datetime usages case 'date': $usageInstance = new UsageDate($usage); + break; case 'time': $usageInstance = new UsageTime($usage); + break; case 'email': $usageInstance = new UsageEmail($usage); + break; case 'hash': break; // binary usages diff --git a/lib/equal/orm/usages/Usage.class.php b/lib/equal/orm/usages/Usage.class.php index 5ad2fef56..0117caa26 100644 --- a/lib/equal/orm/usages/Usage.class.php +++ b/lib/equal/orm/usages/Usage.class.php @@ -51,6 +51,11 @@ class Usage { */ protected $size = 0; + /** + * Return the constraints descriptors, according to the Usage instance. + * Since `function` properties returned by this method expect a non-static context, + * using the ORM, those callbacks are bound to a Usage instance using `bindTo()`. + */ public function getConstraints(): array { return []; } diff --git a/lib/equal/orm/usages/UsageAmount.class.php b/lib/equal/orm/usages/UsageAmount.class.php index 37cdf9d73..984599170 100644 --- a/lib/equal/orm/usages/UsageAmount.class.php +++ b/lib/equal/orm/usages/UsageAmount.class.php @@ -45,10 +45,7 @@ public function getConstraints(): array { case 'money': case 'percent': case 'rate': - if(preg_match('/^[+-]?[0-9]{0,9}(\.[0-9]{0,'.$scale.'})?$/', (string) $value)) { - return false; - } - break; + return preg_match('/^[+-]?[0-9]{0,9}(\.[0-9]{0,'.$scale.'})?$/', (string) $value); } return true; } diff --git a/lib/equal/orm/usages/UsageCountry.class.php b/lib/equal/orm/usages/UsageCountry.class.php index 249441d2a..b4f3322c9 100644 --- a/lib/equal/orm/usages/UsageCountry.class.php +++ b/lib/equal/orm/usages/UsageCountry.class.php @@ -20,7 +20,7 @@ public function getConstraints(): array { ] ]; } - // subtype is expected to be iso-3166 + // subtype is expected to be ISO-3166 switch($this->getLength()) { case '3': return [ @@ -42,7 +42,6 @@ public function getConstraints(): array { } ] ]; - } } diff --git a/lib/equal/orm/usages/UsageCurrency.class.php b/lib/equal/orm/usages/UsageCurrency.class.php index df0bb0ce2..9fa84feaa 100644 --- a/lib/equal/orm/usages/UsageCurrency.class.php +++ b/lib/equal/orm/usages/UsageCurrency.class.php @@ -13,7 +13,7 @@ public function getConstraints(): array { if($this->getSubtype() == 'iso-4217.numeric') { return [ 'invalid_currency' => [ - 'message' => 'Value is not a 3-digits country code (iso-3166-1).', + 'message' => 'Value is not a 3-digits country code (as of iso-3166-1).', 'function' => function($value) { return (in_array($value, ['004','008','010','012','016','020','024','028','031','032','036','040','044','048','050','051','052','056','060','064','068','070','072','074','076','084','086','090','092','096','100','104','108','112','116','120','124','132','136','140','144','148','152','156','158','162','166','170','174','175','178','180','184','188','191','192','196','203','204','208','212','214','218','222','226','231','232','233','234','238','239','242','246','248','250','254','258','260','262','266','268','270','275','','276','288','292','296','300','304','308','312','316','320','324','328','332','334','336','340','344','348','352','356','360','364','368','372','376','380','384','388','392','398','400','404','408','410','414','417','418','422','426','428','430','434','438','440','442','446','450','454','458','462','466','470','474','478','480','484','492','496','498','499','500','504','508','512','516','520','524','528','531','533','534','535','540','548','554','558','562','566','570','574','578','580','581','583','584','585','586','591','598','600','604','608','612','616','620','624','626','630','634','638','642','643','646','652','654','659','660','662','663','666','670','674','678','682','686','688','690','694','702','703','704','705','706','710','716','724','728','729','732','740','744','748','752','756','760','762','764','768','772','776','780','784','788','792','795','796','798','800','804','807','818','826','831','832','833','834','840','850','854','858','860','862','876','882','887','894'])); } @@ -27,7 +27,7 @@ public function getConstraints(): array { default: return [ 'invalid_currency' => [ - 'message' => 'Value is not a 2-letters language code (iso-4217 alpha-3).', + 'message' => 'Value is not a 2-letters language code (as of iso-4217 alpha-3).', 'function' => function($value) { return (in_array($value, ['ADF','ADP','AED','AFA','AFN','ALL','AMD','ANG','AOA','AOK','AON','AOR','ARP','ARS','ATS','AUD','AWG','AZM','AZN','BAM','BBD','BDT','BEF','BGL','BGN','BHD','BIF','BMD','BND','BOB','BOP','BOV','BRL','BRR','BSD','BTN','BWP','BYB','BYR','BYN','BZD','CAD','CDF','CHE','CHF','CHW','CLF','CLP','CNY','COP','COU','CRC','CSD','CSK','CUC','CUP','CVE','CYP','CZK','DEM','DJF','DKK','DOP','DZD','ECS','ECV','EEK','EGP','ERN','ESP','ETB','EUR','FIM','FJD','FKP','FRF','GBP','GEL','GHS','GIP','GMD','GNF','GRD','GTQ','GWP','GYD','HKD','HNL','HRK','HTG','HUF','IDR','IEP','ILS','INR','IQD','IRR','ISK','ITL','JMD','JOD','JPY','KES','KGS','KHR','KMF','KPW','KRW','KZT','KWD','KYD','LAK','LBP','LKR','LRD','LSL','LTL','LUF','LVL','LVR','LYD','MAD','MDL','MGA','MGF','MKD','MMK','MNT','MOP','MRO','MRU','MTL','MUR','MVR','MWK','MXN','MXV','MYR','MZE','MZM','MZN','NAD','NGN','NHF','NIC','NIO','NLG','NOK','NPR','NZD','OMR','PAB','PEN','PES','PGK','PHP','PKR','PLN','PLZ','PTE','PYG','QAR','ROL','RON','RSD','RUB','RWF','SAR','SBD','SCR','SDD','SDG','SDP','SEK','SGD','SHP','SIT','SKK','SLL','SML','SOS','SRD','SSP','STD','SUB','SUR','SVC','SYP','SZL','THB','TJS','TMM','TMT','TND','TOP','TPE','TRL','TRY','TTD','TWD','TZS','UAH','UGX','USD','USN','USS','UYU','UYW','UZS','VAL','VEB','VEF','VES','VND','VUV','WST','XAF','XAG','XAU','XBA','XBB','XBC','XBD','XCD','XDR','XEU','XFO','XFU','XOF','XPD','XPF','XPT','XSU','XUA','YER','YUD','YUM','ZAR','ZMK','ZWD','ZWL','ZWR'])); } diff --git a/lib/equal/orm/usages/UsageDate.class.php b/lib/equal/orm/usages/UsageDate.class.php index 6c901b553..7d2bfda90 100644 --- a/lib/equal/orm/usages/UsageDate.class.php +++ b/lib/equal/orm/usages/UsageDate.class.php @@ -30,7 +30,7 @@ public function getConstraints(): array { case 'day': return [ 'invalid_amount' => [ - 'message' => 'Malformed amount or size overflow.', + 'message' => 'Malformed day value.', 'function' => function($value) { // 2 digits, from 1 to 31 return ($value >= 0 && $value <= 31); @@ -40,7 +40,7 @@ public function getConstraints(): array { case 'month': return [ 'invalid_amount' => [ - 'message' => 'Malformed amount or size overflow.', + 'message' => 'Malformed month value.', 'function' => function($value) { // 2 digits, from 1 to 12 return ($value >= 0 && $value <= 12); @@ -50,7 +50,7 @@ public function getConstraints(): array { case 'year': return [ 'invalid_amount' => [ - 'message' => 'Malformed amount or size overflow.', + 'message' => 'Malformed year value.', 'function' => function($value) { // 4 digits, from 0 to 9999 return ($value >= 0 && $value <= 9999); diff --git a/lib/equal/orm/usages/UsageNumber.class.php b/lib/equal/orm/usages/UsageNumber.class.php index 1a9e3d5e4..f51d024d8 100644 --- a/lib/equal/orm/usages/UsageNumber.class.php +++ b/lib/equal/orm/usages/UsageNumber.class.php @@ -66,13 +66,10 @@ public function getConstraints(): array { switch($this->getSubtype()) { case 'boolean': return [ - 'not_integer' => [ + 'not_boolean' => [ 'message' => 'Value is not a boolean.', 'function' => function($value) { - if(!preg_match('/^[0-1]$/', (string) $value)) { - return false; - } - return true; + return preg_match('/^[0-1]$/', (string) intval($value)); } ] ]; @@ -84,10 +81,7 @@ public function getConstraints(): array { $len = intval($this->getLength()); // if length is empty, default to 18 (max) $len = ($len)?$len:18; - if(!preg_match('/^[+-]?[0-9]{0,'.$len.'}$/', (string) $value)) { - return false; - } - return true; + return preg_match('/^[+-]?[0-9]{0,'.$len.'}$/', (string) $value); } ] ]; @@ -106,10 +100,7 @@ public function getConstraints(): array { // expected len format is `precision.scale` $scale = $this->getScale(); $integers = $this->getPrecision(); - if(preg_match('/^[+-]?[0-9]{0,'.$integers.'}(\.[0-9]{1,'.$scale.'})?$/', (string) $value)) { - return false; - } - return true; + return preg_match('/^[+-]?[0-9]{0,'.$integers.'}(\.[0-9]{1,'.$scale.'})?$/', (string) $value); } ] ]; @@ -121,10 +112,7 @@ public function getConstraints(): array { $len = intval($this->getLength()); // if length is empty, default to 255 (max) $len = ($len)?$len:255; - if(preg_match('/^[0-9A-F]{0,'.$len.'}$/', (string) $value)) { - return false; - } - return true; + return preg_match('/^[0-9A-F]{0,'.$len.'}$/', (string) $value); } ] ]; diff --git a/lib/equal/orm/usages/UsageText.class.php b/lib/equal/orm/usages/UsageText.class.php index 56a82cec2..ff3988da2 100644 --- a/lib/equal/orm/usages/UsageText.class.php +++ b/lib/equal/orm/usages/UsageText.class.php @@ -18,12 +18,6 @@ public function __construct(string $usage_str) { public function getConstraints(): array { return [ - 'not_string_type' => [ - 'message' => 'Value is not a string.', - 'function' => function($value) { - return (gettype($value) == 'string'); - } - ], 'size_exceeded' => [ 'message' => 'String exceeds usage length constraint.', 'function' => function($value) { @@ -42,16 +36,16 @@ public function getConstraints(): array { case 'plain': break; case 'html': - $doc = new DOMDocument(); + $doc = new \DOMDocument(); libxml_use_internal_errors(true); $doc->loadHTML($value); return (empty(libxml_get_errors())); break; case 'xml': // #todo - check XML validity - $xml = new XMLReader(); + $xml = new \XMLReader(); $xml->xml($value); - $xml->setParserProperty(XMLReader::VALIDATE, true); + $xml->setParserProperty(\XMLReader::VALIDATE, true); return $xml->isValid(); case 'markdown': // #todo - check markdown validity diff --git a/lib/equal/services/Container.class.php b/lib/equal/services/Container.class.php index 24041178f..dbae7be12 100644 --- a/lib/equal/services/Container.class.php +++ b/lib/equal/services/Container.class.php @@ -64,17 +64,26 @@ private function inject($dependency) { if(count($parameters)) { foreach($parameters as $parameter) { - // #deprecated - // $constructor_dependency = $parameter->getClass()->getName(); - $constructor_dependency = $parameter->getType()->getName(); - // #todo - missing cyclic dependency check - $res = $this->inject($constructor_dependency); - if(count($res[1])) { - $unresolved_dependencies = array_merge($unresolved_dependencies, $res[1]); + // #memo - ReflectionType::__toString has been deprecated PHP 7.4 and undeprecated in 8.0 + /** @var ReflectionType */ + $type_name = @ (string) $parameter->getType(); + // ignore scalar types + if(empty($type_name) || in_array($type_name, ['array', 'bool', 'callable', 'float', 'int', 'null', 'object', 'string', 'false', 'iterable', 'mixed', 'never', 'true', 'void'])) { continue; } - if($res[0] instanceof $constructor_dependency) { - $dependencies_instances[] = $res[0]; + if($type_name == 'equal\services\Container') { + $dependencies_instances[] = $this; + } + else { + // #todo - add support for cyclic dependency detection + $res = $this->inject($type_name); + if(count($res[1])) { + $unresolved_dependencies = array_merge($unresolved_dependencies, $res[1]); + continue; + } + if($res[0] instanceof $type_name) { + $dependencies_instances[] = $res[0]; + } } } } diff --git a/packages/core/actions/config/create-workflow.php b/packages/core/actions/config/create-workflow.php index ed98cd6d1..ea3ba1257 100644 --- a/packages/core/actions/config/create-workflow.php +++ b/packages/core/actions/config/create-workflow.php @@ -4,7 +4,6 @@ Some Rights Reserved, Cedric Francoys, 2010-2021 Licensed under GNU LGPL 3 license */ -use PhpParser\ParserFactory; list($params, $providers) = eQual::announce([ 'description' => "Add an empty workflow to the given class by creating a `getWorkflow()` method (if not defined yet).", @@ -33,36 +32,27 @@ */ list($context, $orm) = [ $providers['context'], $providers['orm'] ]; - -// retrieve target entity +// force class autoload $entity = $orm->getModel($params['entity']); if(!$entity) { throw new Exception("unknown_entity", QN_ERROR_INVALID_PARAM); } -$reflectionClass = new ReflectionClass($entity::getType()); -if($reflectionClass->getMethod('getWorkflow')->class == $entity::getType()) { +$class = new ReflectionClass($entity::getType()); +if($class->getMethod('getWorkflow')->class == $entity::getType()) { throw new Exception("duplicate_method", QN_ERROR_INVALID_PARAM); } -$parts = explode('\\', $params['entity']); -// Get the package name from the first part of the string -$package = array_shift($parts);// Get the package name from the first part of the string -// Get the file name from the last part of the string -$class_name = array_pop($parts); -// Get the class path from the remaining part -$class_path = implode('/', $parts); - -// Create all the object to use for using PhpParser -$parser = (new ParserFactory)->create(ParserFactory::PREFER_PHP7); -$printer = new PhpParser\PrettyPrinter\Standard; - -// Get the full path of the file -$file = QN_BASEDIR."/packages/{$package}/classes/{$class_path}/{$class_name}.class.php"; - -// get php code from original file ... +$file = $class->getFileName(); $code = file_get_contents($file); +// generate replacement code for the getWorkflow method +$workflow_code = ''. + " public static function getWorkflow() {\n". + " return [];\n". + " }\n". + "\n"; + // find the closing curly-bracket of the class $pos = strrpos($code, '}'); @@ -70,33 +60,13 @@ throw new Exception('malformed_file', QN_ERROR_UNKNOWN); } -$code = substr_replace($code, 'public static function getWorkflow() {return [];} }', $pos, 1); - -// ... and parse it to create an AST -$stmt = $parser->parse($code); +$result = substr_replace($code, $workflow_code, $pos, 0); -// Pretty print the modified AST ... -$result = $printer->prettyPrintFile($stmt); - -// ... and write back the code to the file -if(file_put_contents($file, $result) === false) { +// write back the code to the source file +if(file_put_contents($file, rtrim($result)."\n") === false) { throw new Exception('io_error', QN_ERROR_UNKNOWN); } -try { - // apply coding standards (ecs.php is expected in QN_BASEDIR) - $command = 'php ./vendor/bin/ecs check "' . str_replace('\\', '/', $file) . '" --fix'; - if(exec($command) === false) { - throw new Exception('command_failed', QN_ERROR_UNKNOWN); - } -} -catch(Exception $e) { - trigger_error("PHP::unable to beautify rendered file ($file): ".$e->getMessage(), QN_REPORT_INFO); -} - -$result = file_get_contents($file); - $context->httpResponse() ->status(204) - ->body($result) ->send(); diff --git a/packages/core/actions/config/update-uml.php b/packages/core/actions/config/update-uml.php index 78d2a4aca..82a06bda1 100644 --- a/packages/core/actions/config/update-uml.php +++ b/packages/core/actions/config/update-uml.php @@ -9,7 +9,7 @@ 'required' => true ], 'path' => [ - 'decription' => 'relative path to the file from packages/{pkg}/', + 'description' => 'relative path to the file from packages/{pkg}/', 'type' => 'string', 'required' => true ], @@ -23,7 +23,7 @@ 'type' => 'string', 'required' => true, 'selection' => [ - 'or' + 'erd' ] ] ], @@ -68,13 +68,13 @@ $str_payload =$params['payload']; -if(!endsWith($filename,".{$params["type"]}.equml")) { - $filename = $filename.".{$params["type"]}.equml"; +if(!endsWith($filename,".{$params["type"]}.json")) { + $filename = $filename.".{$params["type"]}.json"; } if(!is_dir(QN_BASEDIR."/packages/{$package}/uml/{$path}")) { $response_code = 201; - if(!mkdir(QN_BASEDIR."/packages/{$package}/uml/{$path}",0775,true)) { + if(!mkdir(QN_BASEDIR."/packages/{$package}/uml/{$path}", 0775, true)) { throw new Exception('io_error'.QN_BASEDIR."/packages/{$package}/uml/{$path}", QN_ERROR_INVALID_CONFIG); } } @@ -82,11 +82,13 @@ if($response_code === 200 && !file_exists(QN_BASEDIR."/packages/{$package}/uml/{$path}/{$filename}")) { $response_code = 201; } -// Create file + +// create file $f = fopen(QN_BASEDIR."/packages/{$package}/uml/{$path}/{$filename}","w"); if(!$f) { throw new Exception('io_error', QN_ERROR_INVALID_CONFIG); } + fputs($f,$str_payload); fclose($f); @@ -95,7 +97,7 @@ $context->httpResponse() ->body($result) ->status($response_code) - ->send(); + ->send(); function endsWith( $haystack, $needle ) { $length = strlen( $needle ); diff --git a/packages/core/actions/config/update-workflow.php b/packages/core/actions/config/update-workflow.php index 82dec5fa3..65f8f6aee 100644 --- a/packages/core/actions/config/update-workflow.php +++ b/packages/core/actions/config/update-workflow.php @@ -9,11 +9,6 @@ list($params, $providers) = eQual::announce([ 'description' => "Translate a workflow definition of a given entity to a PHP method and store it in related file.", 'help' => "This controller rely on the PHP binary. In order to make them work, sure the PHP binary is present in the PATH.", - 'response' => [ - 'content-type' => 'text/plain', - 'charset' => 'UTF-8', - 'accept-origin' => '*' - ], 'params' => [ 'entity' => [ 'description' => 'Name of the entity (class).', @@ -26,6 +21,11 @@ 'required' => true ] ], + 'response' => [ + 'content-type' => 'text/plain', + 'charset' => 'UTF-8', + 'accept-origin' => '*' + ], 'providers' => ['context', 'orm'] ]); @@ -35,88 +35,96 @@ */ list($context, $orm) = [$providers['context'], $providers['orm']]; -// Create all the object to use for using PhpParser -$parser = (new ParserFactory)->create(ParserFactory::PREFER_PHP7); -$nodeFinder = new NodeFinder; -$traverser = new NodeTraverser; -$prettyPrinter = new PhpParser\PrettyPrinter\Standard; -// Get the parts of the entity string, separated by backslashes -$parts = explode('\\', $params['entity']); -// Get the package name from the first part of the string -$package = array_shift($parts);// Get the package name from the first part of the string -// Get the file name from the last part of the string -$filename = array_pop($parts); -// Get the class path from the remaining part -$class_path = implode('/', $parts); - -$getColumns = null; -$code_php = null; +// force class autoload +$entity = $orm->getModel($entity::getType()); +if(!$entity) { + throw new Exception("unknown_entity", QN_ERROR_INVALID_PARAM); +} -// Decode the JSON string to a PHP object -$code_php = $params['payload']; -// Get a string representation from the code_php variable, with backslashes escaped -$code_string = str_replace("\\\\", "\\", var_export($code_php, true)); -// Create a virtual file holing the default structure with the parsed code and generate a minimal AST -$ast_temp = $parser->parse("findFirst( - $ast_temp, - function(Node $node) { - return isset($node->name->name) - && $node->name->name === "getWorkflow"; - } -); +$class = new ReflectionClass($entity::getType()); -// Add a visitor hijack the getWorkflow method and update its content with new schema -$traverser->addVisitor( - new class($nodeGetWorkflow, "getWorkflow") extends NodeVisitorAbstract { - private $node; - private $target; - public function __construct($node, $target) { - $this->node = $node; - $this->target = $target; - } - public function leaveNode(Node $node) { - if ( - isset($node->name->name) - && $node->name->name === $this->target - ) { - return $this->node; - } +if(!$class->hasMethod('getWorkflow')) { + throw new Exception('missing_workflow', QN_ERROR_UNKNOWN); +} - return null; - } - } - ); +$getWorkflow = new ReflectionMethod($params['entity'], 'getWorkflow'); -// Get the full path of the file -$file = QN_BASEDIR."/packages/{$package}/classes/{$class_path}/{$filename}.class.php"; -// Get the code from the original file ... +// retrieve content of the source file of the targeted entity +$file = $class->getFileName(); $code = file_get_contents($file); -// ... and parse it to create an AST -$stmtOriginal = $parser->parse($code); -// Update the AST by using visitors attached to the traverser -$stmtModified = $traverser->traverse($stmtOriginal); -// Pretty print the modified AST ... -$result = $prettyPrinter->prettyPrintFile($stmtModified); -// ... and write back the code to the file -if(file_put_contents($file, $result) === false) { - throw new Exception('io_error', QN_ERROR_UNKNOWN); -} +$lines = explode("\n", $code); + +// retrieve start and end lines of getWorkflow declaration +$start_index = $getWorkflow->getStartLine() - 1; +$end_index = $getWorkflow->getEndLine() - 1; + +// generate replacement code for the getWorkflow method +$workflow_code = ''. + " public static function getWorkflow() {\n". + " return ".array_export($params['payload'], 4, 2, true).";\n". + " }"; -try { - // apply coding standards (ecs.php is expected in QN_BASEDIR) - $command = 'php ./vendor/bin/ecs check "' . str_replace('\\', '/', $file) . '" --fix'; - if(exec($command) === false) { - throw new Exception('command_failed', QN_ERROR_UNKNOWN); +$result = ''; + +foreach($lines as $index => $line) { + if($index < $start_index) { + $result .= $line."\n"; + } + else { + if($index == $start_index) { + $result .= $workflow_code."\n"; + } + else { + if($index > $end_index) { + $result .= $line."\n"; + } + } } - $result = file_get_contents($file); } -catch(Exception $e) { - trigger_error("PHP::unable to beautify rendered file ($file): ".$e->getMessage(), QN_REPORT_INFO); + +// write back the code to the source file +if(file_put_contents($file, rtrim($result)."\n") === false) { + throw new Exception('io_error', QN_ERROR_UNKNOWN); } $context->httpResponse() ->status(204) - ->body($result) ->send(); + +/** + * Use var_export() native function and apply a few regex replacements to improve the syntax. + * Note : this strategy is preferred to the use of PhpParser since it allows to modify only the portion of code targeted by the getWorkflow method. + * + * @param array $array Array to be converted to PHP code. + * @param int $indent_spaces Number of spaces to use for code indentation. + * @param int $pad_indents Number of indents each line must be prefixed with. + * @param bool $ignore_first_indent Flag for disabling indent for first line. + */ +function array_export($array, $indent_spaces = 4, $pad_indents = 0, $ignore_first_indent=false) { + // convert to PHP code + $export = var_export($array, true); + // minimalist code 'beautify' + $patterns = [ + // convert to square bracket notation + "/array \(/" => '[', + "/^([ ]*)\)(,?)$/m" => '$1]$2', + "/=>[ ]?\n[ ]+\[/" => '=> [', + "/([ ]*)(\'[^\']+\') => ([\[\'])/" => '$1$2 => $3', + // remove explicit numeric index + "/[0-9]+ => /" => '' + ]; + $result = preg_replace(array_keys($patterns), array_values($patterns), $export); + // indent lines + $lines = explode("\n", $result); + foreach($lines as $index => $line) { + if(!$ignore_first_indent || $index > 0) { + $code = ltrim($line); + // produced PHP code is 2 spaces indented + $indents = (strlen($line) - strlen($code)) / 2; + $lines[$index] = str_pad('', $pad_indents*$indent_spaces, ' '). + str_pad('', $indents*$indent_spaces, ' '). + $code; + } + } + return implode("\n", $lines); +} diff --git a/packages/core/actions/init/db.php b/packages/core/actions/init/db.php index 5c7dd9a5a..ac249a8ce 100644 --- a/packages/core/actions/init/db.php +++ b/packages/core/actions/init/db.php @@ -4,7 +4,7 @@ Some Rights Reserved, Cedric Francoys, 2010-2021 Licensed under GNU LGPL 3 license */ -use equal\db\DBConnection; +use equal\db\DBConnector; list($params, $providers) = eQual::announce([ @@ -19,14 +19,14 @@ eQual::run('do', 'test_db-connectivity'); // create Master database -$db = DBConnection::getInstance(constant('DB_HOST'), constant('DB_PORT'), constant('DB_NAME'), constant('DB_USER'), constant('DB_PASSWORD'), constant('DB_DBMS'))->connect(false); +$db = DBConnector::getInstance()->connect(false); $db->createDatabase(constant('DB_NAME')); // create replica members, if any if(defined('DB_REPLICATION') && constant('DB_REPLICATION') != 'NO') { $i = 1; while(defined('DB_'.$i.'_HOST') && defined('DB_'.$i.'_PORT') && defined('DB_'.$i.'_USER') && defined('DB_'.$i.'_PASSWORD') && defined('DB_'.$i.'_NAME')) { - $db = DBConnection::getInstance(constant('DB_'.$i.'_HOST'), constant('DB_'.$i.'_PORT'), constant('DB_'.$i.'_NAME'), constant('DB_'.$i.'_USER'), constant('DB_'.$i.'_PASSWORD'), constant('DB_DBMS'))->connect(false); + $db = DBConnector::getInstance(constant('DB_'.$i.'_HOST'), constant('DB_'.$i.'_PORT'), constant('DB_'.$i.'_NAME'), constant('DB_'.$i.'_USER'), constant('DB_'.$i.'_PASSWORD'), constant('DB_DBMS'))->connect(false); $db->createDatabase(constant('DB_'.$i.'_NAME')); ++$i; } diff --git a/packages/core/actions/init/import.php b/packages/core/actions/init/import.php new file mode 100644 index 000000000..3339f1510 --- /dev/null +++ b/packages/core/actions/init/import.php @@ -0,0 +1,333 @@ + + Some Rights Reserved, eQual framework, 2010-2024 + Original author(s): Lucas LAURENT + License: GNU LGPL 3 license +*/ +use equal\db\DBConnection; +use equal\db\DBManipulator; +use equal\db\DBConnector; + +list($params, $providers) = eQual::announce([ + 'description' => 'Import data from a database to eQual database for a given package.', + 'help' => 'Needs a configuration file `import-config.json` in the `init` folder of the given package.', + 'constants' => ['DB_HOST', 'DB_PORT', 'DB_NAME', 'DB_USER', 'DB_PASSWORD', 'DB_DBMS'], + 'params' => [ + 'db_dbms' => [ + 'type' => 'string', + 'default' => 'MYSQL' + ], + + 'db_host' => [ + 'type' => 'string', + 'required' => true + ], + + 'db_port' => [ + 'type' => 'integer', + 'required' => true + ], + + 'db_user' => [ + 'type' => 'string', + 'required' => true + ], + + 'db_password' => [ + 'type' => 'string', + 'required' => true + ], + + 'db_name' => [ + 'type' => 'string', + 'required' => true + ], + + 'db_charset' => [ + 'type' => 'string', + 'default' => 'utf8mb4' + ], + + 'db_collation' => [ + 'type' => 'string', + 'default' => 'utf8mb4_unicode_ci' + ], + + 'package' => [ + 'type' => 'string', + 'required' => true + ], + + 'entity' => [ + 'type' => 'string' + ] + ], + 'response' => [ + 'content-type' => 'application/json', + 'charset' => 'utf-8', + 'accept-origin' => '*' + ], + 'providers' => ['context', 'adapt'] +]); + +/** + * @var \equal\php\Context $context + * @var \equal\data\DataAdapterProvider $dap + */ +list($context, $dap) = [$providers['context'], $providers['adapt']]; + +$getImportConfig = function($config_file_path): array { + $config_file_content = file_get_contents($config_file_path); + if(!$config_file_content) { + throw new Exception('Missing import config file ' . $config_file_path, QN_ERROR_INVALID_CONFIG); + } + + $import_config = json_decode($config_file_content, true); + if(!is_array($import_config)) { + throw new Exception('Invalid import configuration file', QN_ERROR_INVALID_CONFIG); + } + + return $import_config; +}; + +$createOldDbConnection = function(string $dbms, string $host, int $port, string $name, string $user, string $password, string $charset, string $collation) { + $db_connection = DBConnection::create($dbms,$host, $port, $name, $user, $password, $charset, $collation); + + $db_connection->connect(); + if(!$db_connection->connected()) { + throw new Exception( + 'Unable to establish connection to DBMS host (wrong credentials) for old connection', + QN_ERROR_INVALID_CONFIG + ); + } + + return $db_connection; +}; + +$createNewDbConnection = function() { + $db_connection = DBConnector::getInstance(); + + $db_connection->connect(); + if(!$db_connection->connected()) { + throw new Exception('Unable to establish connection to DBMS host (wrong credentials)', QN_ERROR_INVALID_CONFIG); + } + + return $db_connection; +}; + +$castOldDbRow = function(array $old_item, array $old_table_fields_config): array { + $old_item_casted = []; + foreach($old_item as $key => $column_value) { + switch($old_table_fields_config[$key]) { + case 'integer': + $old_item_casted[$key] = (int) $column_value; + break; + case 'float': + $old_item_casted[$key] = (float) $column_value; + break; + case 'boolean': + $old_item_casted[$key] = (bool) $column_value; + break; + case 'date': + $old_item_casted[$key] = strtotime($column_value); + break; + default: + $old_item_casted[$key] = $column_value; + break; + } + } + + return $old_item_casted; +}; + +$createNewItemFromOld = function(array $config, DBManipulator $old_db_connection, array $old_item): array { + $item = [ + 'creator' => QN_ROOT_USER_ID, + 'created' => time(), + 'modifier' => QN_ROOT_USER_ID, + 'modified' => time(), + 'deleted' => false, + 'state' => 'instance' + ]; + + foreach($config['data_map'] as $new_key => $import_conf) { + if(is_string($import_conf)) { + $import_conf = [ + 'type' => 'field', + 'field' => $import_conf + ]; + } + + $imp_confs = isset($import_conf['type']) ? [$import_conf] : $import_conf; + $field = $imp_confs[0]['field'] ?? null; + + if(!$field) { + continue; + } + + $previous_value = $old_item[$field] ?? null; + foreach($imp_confs as $imp_conf) { + switch($imp_conf['type']) { + case 'value': + $item[$new_key] = $import_conf['value']; + break; + case 'field': + $item[$new_key] = $previous_value; + break; + case 'computed': + $item[$new_key] = $imp_conf['value']; + foreach($imp_conf['fields'] as $f) { + $item[$new_key] = str_replace('%'.$f.'%', $old_item[$f], $item[$new_key]); + } + break; + case 'cast': + switch($imp_conf['cast']) { + case 'integer': + $item[$new_key] = (int) $previous_value; + break; + case 'boolean': + $item[$new_key] = (bool) $previous_value; + break; + case 'string': + $item[$new_key] = (string) $previous_value; + break; + } + break; + case 'round': + $item[$new_key] = round($previous_value); + break; + case 'multiply': + $item[$new_key] = $previous_value * $import_conf['by']; + break; + case 'divide': + $item[$new_key] = $previous_value / $import_conf['by']; + break; + case 'field-contains': + $item[$new_key] = strpos($previous_value, $imp_conf['value']) !== false; + break; + case 'field-does-not-contain': + $item[$new_key] = strpos($previous_value, $imp_conf['value']) === false; + break; + case 'map-value': + $match_found = false; + foreach($imp_conf['map'] as $map_item) { + if($map_item['old'] != $previous_value) { + continue; + } + + $item[$new_key] = $map_item['new']; + $match_found = true; + break; + } + + if(!$match_found) { + $item[$new_key] = $previous_value; + } + + break; + case 'query': + if(is_null($previous_value)) { + break; + } + + $query = $imp_conf['query']; + $resRel = $old_db_connection->sendQuery( + 'SELECT `' . $query['field'] . '` from `' . $query['table'] . '` WHERE `' . ($query['where_field'] ?? 'id') . '` = ' . $previous_value . ' LIMIT 1;' + ); + + if($relRow = $old_db_connection->fetchArray($resRel)) { + $item[$new_key] = $relRow[$query['field']]; + } + else { + $item[$new_key] = null; + } + + break; + } + + $previous_value = $item[$new_key]; + } + } + + return $item; +}; + +/** @var \equal\data\adapt\DataAdapter */ +$adapter = $dap->get('sql'); + +$import_config = $getImportConfig( + QN_BASEDIR . '/packages/' . $params['package'] . '/init/import-config.json' +); + +/** @var DBManipulator */ +$old_db_connection = $createOldDbConnection( + $params['db_dbms'], + $params['db_host'], + $params['db_port'], + $params['db_name'], + $params['db_user'], + $params['db_password'], + $params['db_charset'], + $params['db_collation'] + ); + +/** @var DBManipulator */ +$new_db_connection = $createNewDbConnection(); + +$limit = 500; + +foreach($import_config as $config) { + if(isset($params['entity']) && $params['entity'] !== $config['entity']) { + continue; + } + + $new_table_name = str_replace('\\', '_', $config['entity']); + + $offset = 0; + $remaining_data = true; + while($remaining_data) { + $res = $old_db_connection->getRecords( + $config['old_table']['name'], + array_keys($config['old_table']['fields']), + null, + $config['old_table']['conditions'] ?? null, + $config['old_table']['id_field'] ?? 'id', + [], + $offset, + $limit + ); + + if($old_db_connection->getAffectedRows() < $limit) { + $remaining_data = false; + } + + $items = []; + while ($row = $old_db_connection->fetchArray($res)) { + $old_item = $castOldDbRow($row, $config['old_table']['fields']); + $items[] = $createNewItemFromOld($config, $old_db_connection, $old_item); + } + + if(!empty($items)) { + foreach($items as &$item) { + $item['created'] = $adapter->adaptOut($item['created'], 'datetime'); + $item['modified'] = $adapter->adaptOut($item['modified'], 'datetime'); + } + + $new_db_connection->addRecords( + $new_table_name, + array_keys($items[0]), + $items + ); + } + + $offset += $limit; + } +} + +$old_db_connection->disconnect(); +$new_db_connection->disconnect(); + +$context->httpResponse() + ->body(['success' => true]) + ->send(); diff --git a/packages/core/actions/init/package.php b/packages/core/actions/init/package.php index dbe746aeb..61aedeef3 100644 --- a/packages/core/actions/init/package.php +++ b/packages/core/actions/init/package.php @@ -4,7 +4,7 @@ Some Rights Reserved, Cedric Francoys, 2010-2021 Licensed under GNU GPL 3 license */ -use equal\db\DBConnection; +use equal\db\DBConnector; use equal\fs\FSManipulator as FS; use equal\orm\Field; @@ -54,7 +54,7 @@ eQual::run('do', 'test_db-access'); // retrieve connection object -$db = DBConnection::getInstance(constant('DB_HOST'), constant('DB_PORT'), constant('DB_NAME'), constant('DB_USER'), constant('DB_PASSWORD'), constant('DB_DBMS'))->connect(); +$db = DBConnector::getInstance(constant('DB_HOST'), constant('DB_PORT'), constant('DB_NAME'), constant('DB_USER'), constant('DB_PASSWORD'), constant('DB_DBMS'))->connect(); if(!$db) { throw new Exception('missing_database', QN_ERROR_INVALID_CONFIG); diff --git a/packages/core/actions/model/correct.php b/packages/core/actions/model/correct.php index 7a533bc69..d4691c949 100644 --- a/packages/core/actions/model/correct.php +++ b/packages/core/actions/model/correct.php @@ -52,13 +52,13 @@ // retrieve target entity -$entity = $orm->getModel($params['entity']); -if(!$entity) { +$model = $orm->getModel($params['entity']); +if(!$model) { throw new Exception("unknown_entity", QN_ERROR_INVALID_PARAM); } // get the complete schema of the object (including special fields) -$schema = $entity->getSchema(); +$schema = $model->getSchema(); // adapt received fields names for dot notation support $fields = []; @@ -66,7 +66,8 @@ if(!isset($schema[$field])) { continue; } - $f = new Field($schema[$field]); + /** @var equal\orm\Field */ + $f = $model->getField($field); $fields[$field] = $adapter->adaptIn($value, $f->getUsage()); } diff --git a/packages/core/actions/model/create.php b/packages/core/actions/model/create.php index 1e6032780..f5ec2a69f 100644 --- a/packages/core/actions/model/create.php +++ b/packages/core/actions/model/create.php @@ -67,21 +67,25 @@ $adapter = $dap->get('json'); // fields and values have been received as a raw array : adapt received values according to schema -$entity = $orm->getModel($params['entity']); -if(!$entity) { +$model = $orm->getModel($params['entity']); +if(!$model) { throw new Exception("unknown_entity", QN_ERROR_INVALID_PARAM); } -$schema = $entity->getSchema(); +$schema = $model->getSchema(); try { foreach($params['fields'] as $field => $value) { + if(!isset($schema[$field])) { + continue; + } // drop empty and unknown fields if(is_null($value) || !isset($schema[$field])) { unset($params['fields'][$field]); continue; } - $f = new Field($schema[$field]); + /** @var equal\orm\Field */ + $f = $model->getField($field); $params['fields'][$field] = $adapter->adaptIn($value, $f->getUsage()); } } diff --git a/packages/core/actions/model/import.php b/packages/core/actions/model/import.php index bd13d5393..596d4071c 100644 --- a/packages/core/actions/model/import.php +++ b/packages/core/actions/model/import.php @@ -101,7 +101,8 @@ foreach($class['data'] as $odata) { foreach($odata as $field => $value) { - $f = new Field($schema[$field]); + /** @var equal\orm\Field */ + $f = $model->getField($field); $odata[$field] = $adapter->adaptIn($value, $f->getUsage()); } if(isset($odata['id'])) { diff --git a/packages/core/actions/model/onchange.php b/packages/core/actions/model/onchange.php index 98d3193c5..b108b15e2 100644 --- a/packages/core/actions/model/onchange.php +++ b/packages/core/actions/model/onchange.php @@ -77,7 +77,8 @@ // adapt fields in $values array foreach($values as $field => $value) { try { - $f = new Field($schema[$field]); + /** @var equal\orm\Field */ + $f = $model->getField($field); // adapt received values based on their type (as defined in schema) $values[$field] = $adapter->adaptIn($value, $f->getUsage()); } diff --git a/packages/core/actions/model/update.php b/packages/core/actions/model/update.php index b239831af..66164563a 100644 --- a/packages/core/actions/model/update.php +++ b/packages/core/actions/model/update.php @@ -79,6 +79,7 @@ // adapt received values for parameter 'fields' (which are still formatted as text) $schema = $model->getSchema(); + // remove unknown fields $fields = array_filter($params['fields'], function($field) use ($schema){ return isset($schema[$field]); @@ -86,8 +87,10 @@ ARRAY_FILTER_USE_KEY ); - foreach($fields as $field => $value) { + if(!isset($schema[$field])) { + continue; + } $type = $schema[$field]['type']; // drop empty fields (but allow reset to null) if(!is_array($value) && !strlen(strval($value)) && !in_array($type, ['boolean', 'string', 'text']) && !is_null($value) ) { @@ -96,7 +99,8 @@ } try { // adapt received values based on their type (as defined in schema) - $f = new Field($schema[$field]); + /** @var equal\orm\Field */ + $f = $model->getField($field); $fields[$field] = $adapter->adaptIn($value, $f->getUsage()); } catch(Exception $e) { diff --git a/packages/core/actions/test/db-access.php b/packages/core/actions/test/db-access.php index 6bcacf271..6604fddf9 100644 --- a/packages/core/actions/test/db-access.php +++ b/packages/core/actions/test/db-access.php @@ -4,7 +4,7 @@ Some Rights Reserved, Cedric Francoys, 2010-2021 Licensed under GNU GPL 3 license */ -use equal\db\DBConnection; +use equal\db\DBConnector; $params = announce([ 'description' => "Tests access to the database.\nIn case of success, the script simply terminates with a status code of 0 (no output).", @@ -13,7 +13,7 @@ ]); // retrieve connection object -$db = &DBConnection::getInstance(constant('DB_HOST'), constant('DB_PORT'), constant('DB_NAME'), constant('DB_USER'), constant('DB_PASSWORD'), constant('DB_DBMS')); +$db = DBConnector::getInstance(constant('DB_HOST'), constant('DB_PORT'), constant('DB_NAME'), constant('DB_USER'), constant('DB_PASSWORD'), constant('DB_DBMS')); // 1) test connectivity to DBMS service $json = run('do', 'test_db-connectivity'); diff --git a/packages/core/actions/test/db-connectivity.php b/packages/core/actions/test/db-connectivity.php index c96e0dea9..c39948f7a 100644 --- a/packages/core/actions/test/db-connectivity.php +++ b/packages/core/actions/test/db-connectivity.php @@ -4,16 +4,17 @@ Some Rights Reserved, Cedric Francoys, 2010-2021 Licensed under GNU GPL 3 license */ -use equal\db\DBConnection; +use equal\db\DBConnector; $params = eQual::announce([ - 'description' => "Tests connectivity to the DBMS server.\nIn case of success, the script simply terminates with a status code of 0 (no output)", + 'description' => "Tests connectivity to the DBMS server.\n + In case of success, the script simply terminates with a status code of 0 (no output).", 'params' => [], 'constants' => ['DB_HOST', 'DB_PORT', 'DB_NAME', 'DB_USER', 'DB_PASSWORD', 'DB_DBMS'] ]); // retrieve connection object -$db = DBConnection::getInstance(constant('DB_HOST'), constant('DB_PORT'), constant('DB_NAME'), constant('DB_USER'), constant('DB_PASSWORD'), constant('DB_DBMS')); +$db = DBConnector::getInstance(constant('DB_HOST'), constant('DB_PORT'), constant('DB_NAME'), constant('DB_USER'), constant('DB_PASSWORD'), constant('DB_DBMS')); // 1) test access to DBMS service if(!$db->canConnect()) { diff --git a/packages/core/actions/test/package-consistency.php b/packages/core/actions/test/package-consistency.php index ed604796c..4a6a9e8e9 100644 --- a/packages/core/actions/test/package-consistency.php +++ b/packages/core/actions/test/package-consistency.php @@ -4,7 +4,7 @@ Some Rights Reserved, Cedric Francoys, 2010-2021 Licensed under GNU GPL 3 license */ -use equal\db\DBConnection; +use equal\db\DBConnector; // get listing of existing packages $packages = eQual::run('get', 'config_packages'); @@ -369,7 +369,7 @@ // retrieve connection object -$db = DBConnection::getInstance(constant('DB_HOST'), constant('DB_PORT'), constant('DB_NAME'), constant('DB_USER'), constant('DB_PASSWORD'), constant('DB_DBMS'))->connect(); +$db = DBConnector::getInstance(constant('DB_HOST'), constant('DB_PORT'), constant('DB_NAME'), constant('DB_USER'), constant('DB_PASSWORD'), constant('DB_DBMS'))->connect(); if(!$db) { throw new Exception('missing_database', QN_ERROR_INVALID_CONFIG); diff --git a/packages/core/actions/user/pass-update.php b/packages/core/actions/user/pass-update.php index 45dfa1d6a..32a7806f2 100644 --- a/packages/core/actions/user/pass-update.php +++ b/packages/core/actions/user/pass-update.php @@ -6,7 +6,7 @@ */ use core\User; -list($params, $providers) = announce([ +list($params, $providers) = eQual::announce([ 'description' => 'Updates the password related to a user account.', 'response' => [ 'content-type' => 'application/json', diff --git a/packages/core/apps/app/manifest.json b/packages/core/apps/app/manifest.json index 358d9e013..be437ef45 100644 --- a/packages/core/apps/app/manifest.json +++ b/packages/core/apps/app/manifest.json @@ -1,6 +1,6 @@ { "name": "app", - "description": "Standard App to serve as surrogate for Package specific apps.", + "description": "Standard App to serve as surrogate for Package-specific Apps.", "version": "1.0", "authors": ["Cedric Francoys"], "license": "LGPL-3", diff --git a/packages/core/apps/app/version b/packages/core/apps/app/version index d893fa847..bdef2bc5e 100644 --- a/packages/core/apps/app/version +++ b/packages/core/apps/app/version @@ -1 +1 @@ -263fca79556d705632c2bf675c2f6d3a +774003e0a658e1d5098f0f22e7219dca diff --git a/packages/core/apps/app/web.app b/packages/core/apps/app/web.app index b38ac8e5f..d7c962055 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/auth/version b/packages/core/apps/auth/version index 2ff31b6b0..d868fbdde 100644 --- a/packages/core/apps/auth/version +++ b/packages/core/apps/auth/version @@ -1 +1 @@ -a382468b6c29901e622b3e275916bd91 +e78e6de9c057c992b80f8b14bb559a3f diff --git a/packages/core/apps/auth/web.app b/packages/core/apps/auth/web.app index 29b17d6e4..6ed78c92b 100644 Binary files a/packages/core/apps/auth/web.app and b/packages/core/apps/auth/web.app differ diff --git a/packages/core/apps/console.php b/packages/core/apps/console.php index dfa950939..c2ac29e29 100644 --- a/packages/core/apps/console.php +++ b/packages/core/apps/console.php @@ -162,7 +162,7 @@ $function = (isset($stack[$index]['function']))?$stack[$index]['function']:''; $file = (isset($stack[$index]['file']))?$stack[$index]['file']:''; $line = (isset($stack[$index]['line']))?$stack[$index]['line']:''; - $text .= PHP_EOL.($i == ($n - 1))?' └ ':' ├ '; + $text .= PHP_EOL.(($i == ($n - 1))?' └ ':' ├ '); $text .= "{$function} @ {$file} {$line} "; } } @@ -213,7 +213,10 @@ $result[] = "-------------------------------------------------------------------------------------------------------------------------------------------"; } } - $result[] = $thread_display($thread); + foreach(explode(PHP_EOL, $thread_display($thread)) as $line) { + $result[] = $line; + } + $prev_thread_id = $thread['thread_id']; $i++; } diff --git a/packages/core/apps/settings/version b/packages/core/apps/settings/version index 1e00613cf..f6e9510f0 100644 --- a/packages/core/apps/settings/version +++ b/packages/core/apps/settings/version @@ -1 +1 @@ -a6a5979cc76c971fdbfde6cfa6ff8544 +e9c74ec2e37683536b21934e5d97e7a8 diff --git a/packages/core/apps/settings/web.app b/packages/core/apps/settings/web.app index fbf14585c..c576339de 100644 Binary files a/packages/core/apps/settings/web.app and b/packages/core/apps/settings/web.app differ diff --git a/packages/core/apps/welcome/index.html b/packages/core/apps/welcome/index.html new file mode 100644 index 000000000..f5461d21f --- /dev/null +++ b/packages/core/apps/welcome/index.html @@ -0,0 +1,254 @@ + + + + Welcome to eQual + + + + + + + + + +
+
+
+ + +
+
+
+ + + + + \ No newline at end of file diff --git a/packages/core/apps/welcome/manifest.json b/packages/core/apps/welcome/manifest.json index 0e34c9fe8..7ddb96e7a 100644 --- a/packages/core/apps/welcome/manifest.json +++ b/packages/core/apps/welcome/manifest.json @@ -6,12 +6,10 @@ "license": "LGPL-3", "repository": "https://github.com/equalframework/apps-core-welcome.git", "url": "/welcome", - "icon": "home", - "color": "#d2252c", + "icon": "sentiment_satisfied", + "color": "#2b78c3", + "show_in_apps": true, "access": { - "groups": [ - "users" - ] - }, - "show_in_apps": true + "groups": ["users"] + } } diff --git a/packages/core/apps/welcome/web.app b/packages/core/apps/welcome/web.app index 94e8e8ac1..96cad8be5 100644 Binary files a/packages/core/apps/welcome/web.app and b/packages/core/apps/welcome/web.app differ diff --git a/packages/core/apps/workbench/manifest.json b/packages/core/apps/workbench/manifest.json index d661e9edd..cdb7dace7 100755 --- a/packages/core/apps/workbench/manifest.json +++ b/packages/core/apps/workbench/manifest.json @@ -1,12 +1,12 @@ { "name": "Workbench", - "description": "This is the workbench application.", + "description": "The workbench App allows to create, edit, and configure the components of the current installation.", "url": "/workbench", - "icon": "edit_note", - "color": "#d2252c", + "icon": "construction", + "color": "#3a8f50", "access": { "groups": [ - "users", "admins" + "admins" ] }, "show_in_apps": true diff --git a/packages/core/apps/workbench/source b/packages/core/apps/workbench/source index 189b60ebc..9c423378d 160000 --- a/packages/core/apps/workbench/source +++ b/packages/core/apps/workbench/source @@ -1 +1 @@ -Subproject commit 189b60ebc908cc21647a243055045fb78a0defb0 +Subproject commit 9c423378d4a285baedf4cd912efb1bff5176619c diff --git a/packages/core/apps/workbench/version b/packages/core/apps/workbench/version index ba0cfa371..fed04dee0 100644 --- a/packages/core/apps/workbench/version +++ b/packages/core/apps/workbench/version @@ -1 +1 @@ -5c35e5d9b0686cd9cd0b1ef66a6461e2 +767b3dd80844da5fa40bff7b615e11b0 diff --git a/packages/core/apps/workbench/web.app b/packages/core/apps/workbench/web.app index 3a2d0c095..44d170651 100644 Binary files a/packages/core/apps/workbench/web.app and b/packages/core/apps/workbench/web.app differ diff --git a/packages/core/classes/User.class.php b/packages/core/classes/User.class.php index 0d5d3c38b..5cbdfcf5e 100644 --- a/packages/core/classes/User.class.php +++ b/packages/core/classes/User.class.php @@ -211,7 +211,7 @@ public static function calcName($self) { foreach($self as $id => $user) { $parts = explode(' ', str_replace('-', ' ', $user['firstname'].' '.$user['lastname'])); $initials = strtoupper(array_reduce($parts, function($c, $a) {return $c.substr($a, 0, 1);}, '')); - $res = str_replace(['id', 'nickname', 'mail', 'givenname', 'surname', 'initials'], [$id, $user['nickname'], $user['login'], $user['firstname'], $user['lastname'], $initials], $mask); + $res = str_replace(['id', 'nickname', 'mail', 'firstname', 'lastname', 'initials'], [$id, $user['nickname'], $user['login'], $user['firstname'], $user['lastname'], $initials], $mask); // fallback to user ID $result[$id] = (strlen($res) > 0)?$res:$id; } @@ -230,9 +230,9 @@ public static function calcName($self) { */ public static function onupdatePassword($om, $ids, $values, $lang) { $values = $om->read(self::getType(), $ids, ['password']); - foreach($values as $oid => $odata) { - if(substr($odata['password'], 0, 4) != '$2y$') { - $om->update(self::getType(), $oid, ['password' => password_hash($odata['password'], PASSWORD_BCRYPT)]); + foreach($values as $id => $user) { + if(substr($user['password'], 0, 4) != '$2y$') { + $om->update(self::getType(), $id, ['password' => password_hash($user['password'], PASSWORD_BCRYPT)]); } } } @@ -247,7 +247,7 @@ public static function calcFullname($self) { $result = []; $self->read(['firstname', 'lastname']); foreach($self as $id => $user) { - $result[$id] = $user['firstname'].' '.$user['lastname']; + $result[$id] = ucfirst($user['firstname']).' '.mb_strtoupper($user['lastname']); } return $result; } @@ -278,23 +278,23 @@ public static function onchange($event, $values) { if(isset($event['firstname'])) { if(isset($event['lastname'])) { - $result['fullname'] = $event['firstname'].' '.$event['lastname']; + $result['fullname'] = ucfirst($event['firstname']).' '.mb_strtoupper($event['lastname']); } else { if(isset($values['lastname'])) { - $result['fullname'] = $event['firstname'].' '.$values['lastname']; + $result['fullname'] = ucfirst($event['firstname']).' '.mb_strtoupper($values['lastname']); } else { - $result['fullname'] = $event['firstname']; + $result['fullname'] = mb_strtoupper($event['firstname']); } } } else { if(isset($values['firstname'])) { - $result['fullname'] = $values['firstname'].' '.$event['lastname']; + $result['fullname'] = ucfirst($values['firstname']).' '.mb_strtoupper($event['lastname']); } else { - $result['fullname'] = $event['lastname']; + $result['fullname'] = mb_strtoupper($event['lastname']); } } } diff --git a/packages/core/classes/setting/Setting.class.php b/packages/core/classes/setting/Setting.class.php index bd1ba4fa7..2f09e346d 100644 --- a/packages/core/classes/setting/Setting.class.php +++ b/packages/core/classes/setting/Setting.class.php @@ -37,14 +37,16 @@ public static function getColumns() { 'type' => 'string', 'description' => 'Unique code of the parameter.', 'onupdate' => 'onupdateCode', - 'required' => true + 'required' => true, + 'dependencies' => ['name'] ], 'package' => [ 'type' => 'string', 'description' => 'Package which the param refers to, if any.', 'onupdate' => 'onupdatePackage', - 'default' => 'core' + 'default' => 'core', + 'dependencies' => ['name'] ], 'section' => [ @@ -61,7 +63,8 @@ public static function getColumns() { 'foreign_object' => 'core\setting\SettingSection', 'onupdate' => 'onupdateSectionId', 'description' => 'Section the setting relates to.', - 'required' => true + 'required' => true, + 'dependencies' => ['section', 'name'] ], 'title' => [ @@ -134,7 +137,6 @@ public static function getColumns() { } public static function onupdateCode($om, $ids, $values, $lang) { - $om->update(self::getType(), $ids, ['name' => null], $lang); $settings = $om->read(self::getType(), $ids, ['setting_values_ids'], $lang); foreach($settings as $oid => $setting) { $om->update(SettingValue::getType(), $setting['setting_values_ids'], ['name' => null], $lang); @@ -142,7 +144,6 @@ public static function onupdateCode($om, $ids, $values, $lang) { } public static function onupdateSectionId($om, $ids, $values, $lang) { - $om->update(self::getType(), $ids, ['name' => null, 'section' => null], $lang); $settings = $om->read(self::getType(), $ids, ['setting_values_ids'], $lang); foreach($settings as $oid => $setting) { $om->update(SettingValue::getType(), $setting['setting_values_ids'], ['name' => null], $lang); @@ -150,19 +151,18 @@ public static function onupdateSectionId($om, $ids, $values, $lang) { } public static function onupdatePackage($om, $ids, $values, $lang) { - $om->update(self::getType(), $ids, ['name' => null], $lang); $settings = $om->read(self::getType(), $ids, ['setting_values_ids'], $lang); foreach($settings as $oid => $setting) { $om->update(SettingValue::getType(), $setting['setting_values_ids'], ['name' => null], $lang); } } - public static function calcSection($om, $oids, $lang) { + public static function calcSection($om, $ids, $lang) { $result = []; - $settings = $om->read(self::getType(), $oids, ['section_id.code'], $lang); + $settings = $om->read(self::getType(), $ids, ['section_id.code'], $lang); if($settings > 0 && count($settings)) { - foreach($settings as $oid => $odata) { - $result[$oid] = $odata['section_id.code']; + foreach($settings as $id => $setting) { + $result[$id] = $setting['section_id.code']; } } return $result; diff --git a/packages/core/classes/test/Test.class.php b/packages/core/classes/test/Test.class.php new file mode 100644 index 000000000..8cc22a013 --- /dev/null +++ b/packages/core/classes/test/Test.class.php @@ -0,0 +1,50 @@ + + Some Rights Reserved, Cedric Francoys, 2010-2021 + Licensed under GNU GPL 3 license +*/ +namespace core\test; + +use equal\orm\Model; + +/** + * @property alias $name rest + * @property string $login + * @property string $username test bonjour 1 24 5d 344 555 666 + */ +class Test extends Model +{ + public static function getColumns() + { + return [ + 'string_short' => [ + 'type' => 'string', + 'usage' => 'text/plain:9', + 'dependents' => ['tests1_ids' => ['test']] + ], + + 'string_currency' => [ + 'type' => 'string', + 'usage' => 'currency' + ], + + 'float_amount' => [ + 'type' => 'float', + 'usage' => 'amount/money' + ], + + 'datetime' => [ + 'type' => 'datetime' + ], + + 'tests1_ids' => [ + 'type' => 'one2many', + 'foreign_object' => 'core\test\Test1', + 'foreign_field' => 'test_id' + ], + + ]; + } +} diff --git a/packages/core/classes/test/Test1.class.php b/packages/core/classes/test/Test1.class.php new file mode 100644 index 000000000..76a0c62d3 --- /dev/null +++ b/packages/core/classes/test/Test1.class.php @@ -0,0 +1,41 @@ + + Some Rights Reserved, Cedric Francoys, 2010-2021 + Licensed under GNU GPL 3 license +*/ +namespace core\test; + +use equal\orm\Model; + +class Test1 extends Model +{ + public static function getColumns() + { + return [ + 'test' => [ + 'type' => 'computed', + 'result_type' => 'string', + 'function' => 'calcTest', + 'store' => true + ], + + 'test_id' => [ + 'type' => 'many2one', + 'foreign_object' => 'core\test\Test' + ], + ]; + } + + public static function calcTest($self) { + $result = []; + $self->read(['test_id' => ['id', 'string_short']]); + foreach($self as $id => $test1) { + trigger_error("ORM::".'rel:'.$test1['test_id']['id'], QN_REPORT_WARNING); + trigger_error("ORM::".'val:'.$test1['test_id']['string_short'], QN_REPORT_WARNING); + $result[$id] = $test1['test_id']['string_short']; + } + return $result; + } +} diff --git a/packages/core/data/appinfo.php b/packages/core/data/appinfo.php index 0e572039c..40d6c57d5 100644 --- a/packages/core/data/appinfo.php +++ b/packages/core/data/appinfo.php @@ -6,7 +6,7 @@ */ list($params, $providers) = eQual::announce([ - 'description' => 'Retrieve the descriptor of a given App (from manifest), identified by package and app ID.', + 'description' => 'Provide the descriptor of a given App (from manifest), identified by package and app ID.', 'params' => [ 'package' => [ 'type' => 'string', diff --git a/packages/core/data/config/live/routes.php b/packages/core/data/config/live/routes.php index c2c01c5ba..0c1f0a0a2 100644 --- a/packages/core/data/config/live/routes.php +++ b/packages/core/data/config/live/routes.php @@ -6,6 +6,9 @@ */ list($params, $providers) = announce([ 'description' => 'Returns all the routes with priority based on the number.', + 'access' => [ + 'visibility' => 'protected' + ], 'response' => [ 'content-type' => 'application/json', 'charset' => 'UTF-8', diff --git a/packages/core/data/config/uml.php b/packages/core/data/config/uml.php index f4c1c88f9..4a7f07100 100644 --- a/packages/core/data/config/uml.php +++ b/packages/core/data/config/uml.php @@ -1,4 +1,9 @@ + Some Rights Reserved, Cedric Francoys, 2010-2024 + Licensed under GNU LGPL 3 license +*/ list($params, $providers) = eQual::announce([ 'description' => "Attempts to create a new package using a given name.", @@ -9,7 +14,7 @@ 'required' => true ], 'path' => [ - 'decription' => 'relative path to the file from packages/{pkg}/', + 'description' => 'relative path to the file from packages/{pkg}/', 'type' => 'string', 'required' => true ], @@ -18,7 +23,7 @@ 'type' => 'string', 'required' => true, 'selection' => [ - 'or' + 'erd' ] ] ], @@ -61,8 +66,8 @@ $path = str_replace("..","",$path); -if(!endsWith($filename,".{$params["type"]}.equml")) { - $filename = $filename.".{$params["type"]}.equml"; +if(!endsWith($filename,".{$params["type"]}.json")) { + $filename = $filename.".{$params["type"]}.json"; } if(!file_exists(QN_BASEDIR."/packages/{$package}/uml/{$path}/{$filename}")) { @@ -72,7 +77,7 @@ $context->httpResponse() ->body(file_get_contents(QN_BASEDIR."/packages/{$package}/uml/{$path}/{$filename}")) ->status(200) - ->send(); + ->send(); function endsWith( $haystack, $needle ) { $length = strlen( $needle ); diff --git a/packages/core/data/config/umls.php b/packages/core/data/config/umls.php index 632dcf766..bb0995a55 100644 --- a/packages/core/data/config/umls.php +++ b/packages/core/data/config/umls.php @@ -1,5 +1,9 @@ + Some Rights Reserved, Cedric Francoys, 2010-2024 + Licensed under GNU LGPL 3 license +*/ list($params, $providers) = eQual::announce([ 'description' => 'Returns the list of menus defined in a given package, or applicable to a given entity.', @@ -13,9 +17,9 @@ 'description' => 'Type of the UML data', 'type' => 'string', 'selection' => [ - 'or' + 'erd' ] - ] + ] ], 'providers' => ['context', 'orm'] ]); @@ -26,12 +30,12 @@ */ list($context, $orm) = [$providers['context'], $providers['orm']]; -$packages = eQual::run('get','core_config_packages',[]); +$packages = eQual::run('get','core_config_packages', []); $result = []; foreach($packages as $package) { - $result[$package] = recurse_dir(QN_BASEDIR."/packages/{$package}/uml","equml",$params['type']); + $result[$package] = recurse_dir(QN_BASEDIR."/packages/{$package}/uml", "json", $params['type']); } $context->httpResponse() @@ -60,7 +64,7 @@ function endsWith( $haystack, $needle ) { } /** - * #memo - this method highily differs from the one in controllers.php , translations.php and menu.php + * #memo - this method highly differs from the one in controllers.php , translations.php and menu.php */ function recurse_dir($directory, $extension,$type,$parent_name='') { $result = array(); diff --git a/packages/core/data/model/export-pdf.php b/packages/core/data/model/export-pdf.php index 13cb641a6..2a8a08609 100644 --- a/packages/core/data/model/export-pdf.php +++ b/packages/core/data/model/export-pdf.php @@ -518,7 +518,12 @@ function groupObjects($schema, $objects, $group_by) { if(is_array($key)) { if(isset($key['name'])) { $label = $key['name']; - $key = $key['name']; + if(isset($group['order']) && isset($key[$group['order']])) { + $key = str_pad((string) $key[$group['order']], 11, '0', STR_PAD_LEFT); + } + else { + $key = $key['name']; + } } else { $label = ''; diff --git a/packages/core/data/model/menu.php b/packages/core/data/model/menu.php index 03c7875aa..afb30fd2f 100644 --- a/packages/core/data/model/menu.php +++ b/packages/core/data/model/menu.php @@ -29,7 +29,7 @@ /** * @var \equal\php\Context $context */ -list($context, $orm) = [ $providers['context'] ]; +list($context) = [ $providers['context'] ]; $result = []; diff --git a/packages/core/data/model/view.php b/packages/core/data/model/view.php index ece41605b..010f87315 100644 --- a/packages/core/data/model/view.php +++ b/packages/core/data/model/view.php @@ -4,7 +4,7 @@ Some Rights Reserved, Cedric Francoys, 2010-2021 Licensed under GNU LGPL 3 license */ -list($params, $providers) = announce([ +list($params, $providers) = eQual::announce([ 'description' => "Returns the JSON view related to an entity (class model), given a view ID ().", 'params' => [ 'entity' => [ @@ -26,27 +26,151 @@ 'providers' => ['context', 'orm'] ]); - list($context, $orm) = [$providers['context'], $providers['orm']]; +$removeNodes = function (&$layout, $nodes_ids) { + foreach($layout['groups'] ?? [] as $group_index => $group) { + if(isset($group['id']) && in_array($group['id'], $nodes_ids)) { + array_splice($layout['groups'], $group_index, 1); + continue; + } + foreach($group['sections'] ?? [] as $section_index => $section) { + if(isset($section['id']) && in_array($section['id'], $nodes_ids)) { + array_splice($layout['groups'][$group_index]['sections'], $section_index, 1); + continue; + } + foreach($section['rows'] ?? [] as $row_index => $row) { + if(isset($row['id']) && in_array($row['id'], $nodes_ids)) { + array_splice($layout['groups'][$group_index]['sections'][$section_index]['rows'], $row_index, 1); + continue; + } + foreach($row['columns'] ?? [] as $column_index => $column) { + if(isset($column['id']) && in_array($column['id'], $nodes_ids)) { + array_splice($layout['groups'][$group_index]['sections'][$section_index]['rows'][$row_index]['columns'], $column_index, 1); + continue; + } + foreach($row['items'] ?? [] as $item_index => $item) { + if(isset($item['id']) && in_array($item['id'], $nodes_ids)) { + array_splice($layout['groups'][$group_index]['sections'][$section_index]['rows'][$row_index]['columns'][$column_index]['items'], $item_index, 1); + continue; + } + } + } + } + } + } + + foreach($layout['items'] ?? [] as $item_index => $item) { + if(isset($item['id']) && in_array($item['id'], $nodes_ids)) { + array_splice($layout['items'], $item_index, 1); + } + } +}; + +$updateNode = function (&$layout, $id, $node) { + $target = null; + $index = 0; + $target_parent = null; + foreach($layout['groups'] as $group_index => $group) { + if(isset($group['id']) && $group['id'] == $id) { + $target = &$layout['groups'][$group_index]; + break; + } + $target_parent = &$layout['groups'][$group_index]['sections']; + foreach($group['sections'] as $section_index => $section) { + if(isset($section['id']) && $section['id'] == $id) { + $target = &$layout['groups'][$group_index]['sections'][$section_index]; + $index = $section_index; + break 2; + } + $target_parent = &$layout['groups'][$group_index]['sections'][$section_index]['rows']; + foreach($section['rows'] as $row_index => $row) { + if(isset($row['id']) && $row['id'] == $id) { + $target = &$layout['groups'][$group_index]['sections'][$section_index]['rows'][$row_index]; + $index = $row_index; + break 3; + } + $target_parent = &$layout['groups'][$group_index]['sections'][$section_index]['rows'][$row_index]['columns']; + foreach($row['columns'] as $column_index => $column) { + if(isset($column['id']) && $column['id'] == $id) { + $target = &$layout['groups'][$group_index]['sections'][$section_index]['rows'][$row_index]['columns'][$column_index]; + $index = $column_index; + break 4; + } + $target_parent = &$layout['groups'][$group_index]['sections'][$section_index]['rows'][$row_index]['columns'][$column_index]['items']; + foreach($column['items'] as $item_index => $item) { + if(isset($item['id']) && $item['id'] == $id) { + $target = &$layout['groups'][$group_index]['sections'][$section_index]['rows'][$row_index]['columns'][$column_index]['items'][$item_index]; + $index = $item_index; + break 5; + } + } + } + } + } + } + + if(isset($layout['items'])) { + $target_parent = &$layout['items']; + foreach($layout['items'] as $item_index => $item) { + if(isset($item['id']) && $item['id'] == $id) { + $target = &$layout['items'][$item_index]; + $index = $item_index; + } + } + } + + if($target) { + if(isset($node['attributes'])) { + foreach((array) $node['attributes'] as $attribute => $value) { + $target[$attribute] = $value; + } + } + if(isset($node['prepend'])) { + foreach((array) $node['prepend'] as $elem) { + array_unshift($target, $elem); + } + } + if(isset($node['append'])) { + foreach((array) $node['append'] as $elem) { + array_push($target, $elem); + } + } + if($target_parent) { + if(isset($node['before'])) { + array_splice($target_parent, $index, 0, (array) $node['before']); + $index += count((array) $node['before']); + } + if(isset($node['after'])) { + array_splice($target_parent, $index + 1, 0, (array) $node['after']); + } + } + } +}; + + $entity = $params['entity']; list($view_type, $view_name) = explode('.', $params['view_id']); -// retrieve existing view meant for entity (recurse through parents) +if(!in_array($view_type, ['form', 'list', 'chart', 'search', 'report', 'cards', 'dashboard'])) { + throw new Exception('invalid_view_type', EQ_ERROR_INVALID_PARAM); +} + +// pass-1 : retrieve existing view meant for entity (recurse through parents) while(true) { $parts = explode('\\', $entity); $package = array_shift($parts); - $file = array_pop($parts); + $filename = array_pop($parts); $class_path = implode('/', $parts); - $file = QN_BASEDIR."/packages/{$package}/views/{$class_path}/{$file}.{$view_type}.{$view_name}.json"; + $file = QN_BASEDIR."/packages/{$package}/views/{$class_path}/{$filename}.{$view_type}.{$view_name}.json"; if(file_exists($file)) { break; } // fallback to default variant of the view - $file = QN_BASEDIR."/packages/{$package}/views/{$class_path}/{$file}.{$view_type}.default.json"; + $file = QN_BASEDIR."/packages/{$package}/views/{$class_path}/{$filename}.{$view_type}.default.json"; if(file_exists($file)) { break; } @@ -69,10 +193,36 @@ throw new Exception("missing_view", QN_ERROR_UNKNOWN_OBJECT); } -if( ($view = json_decode(@file_get_contents($file), true)) === null) { +if(($view = json_decode(@file_get_contents($file), true)) === null) { throw new Exception("malformed_view_schema", QN_ERROR_INVALID_CONFIG); } +if(!isset($view['layout'])) { + throw new Exception("malformed_view_schema", QN_ERROR_INVALID_CONFIG); +} + +// pass-2 : adapt the view if inheritance is involved +if(isset($view['layout']['extends'])) { + if(!isset($view['layout']['extends']['view'])) { + throw new Exception("malformed_view_schema", QN_ERROR_INVALID_CONFIG); + } + $view_id = $view['layout']['extends']['view']; + $entity = $view['layout']['extends']['entity'] ?? $params['entity']; + if($params['view_id'] == $view_id && $params['entity'] == $entity) { + throw new Exception("cyclic_view_dependency", QN_ERROR_INVALID_CONFIG); + } + $parent_view = eQual::run('get', 'model_view', ['entity' => $entity, 'view_id' => $view_id]); + if(isset($view['layout']['remove'])) { + $removeNodes($parent_view['layout'], (array) $view['layout']['remove']); + } + if(isset($view['layout']['update'])) { + foreach((array) $view['layout']['update'] as $id => $node) { + $updateNode($parent_view['layout'], $id, $node); + } + } + $view['layout'] = $parent_view['layout']; +} + $context->httpResponse() ->body($view) ->send(); diff --git a/packages/core/data/utils/sql-schema.php b/packages/core/data/utils/sql-schema.php index b17e3a815..268bc291d 100644 --- a/packages/core/data/utils/sql-schema.php +++ b/packages/core/data/utils/sql-schema.php @@ -5,7 +5,7 @@ Licensed under GNU LGPL 3 license */ use equal\orm\ObjectManager; -use equal\db\DBConnection; +use equal\db\DBConnector; // get listing of existing packages $packages = eQual::run('get', 'config_packages'); @@ -49,7 +49,7 @@ exit(1); } // retrieve connection object -$db = DBConnection::getInstance(constant('DB_HOST'), constant('DB_PORT'), constant('DB_NAME'), constant('DB_USER'), constant('DB_PASSWORD'), constant('DB_DBMS'))->connect(); +$db = DBConnector::getInstance(constant('DB_HOST'), constant('DB_PORT'), constant('DB_NAME'), constant('DB_USER'), constant('DB_PASSWORD'), constant('DB_DBMS'))->connect(); if(!$db) { throw new Exception('missing_database', QN_ERROR_INVALID_CONFIG); diff --git a/packages/core/i18n/es/User.json b/packages/core/i18n/es/User.json index ab3b94287..132e2b36b 100644 --- a/packages/core/i18n/es/User.json +++ b/packages/core/i18n/es/User.json @@ -112,8 +112,8 @@ "group.user": { "label": "Información del usuario" }, - "user_details": { - "label": "Detalles del usuario" + "section.details": { + "label": "Detalles" }, "section.preferences": { "label": "Preferencias" diff --git a/packages/core/i18n/fr/User.json b/packages/core/i18n/fr/User.json index 53ed8ec22..6f14cade2 100644 --- a/packages/core/i18n/fr/User.json +++ b/packages/core/i18n/fr/User.json @@ -41,8 +41,8 @@ "label.details":{ "label":"Détails de contact" }, - "section.user_details": { - "label": "Section des détails" + "section.details": { + "label": "Détails" }, "identification":{ "label":"Identification" diff --git a/packages/core/i18n/fr/locale.json b/packages/core/i18n/fr/locale.json index 49bfd4e3f..fad7d3be7 100644 --- a/packages/core/i18n/fr/locale.json +++ b/packages/core/i18n/fr/locale.json @@ -47,9 +47,13 @@ "numbers.decimal_separator": ",", "currency.symbol_position": "after", "currency.symbol_separator": " ", + "date.month.full": "MMMM YYYY", + "date.month.long": "MMM YYYY", + "date.month.medium": "MMM YY", + "date.month.short": "MM/YY", "date.short.day": "ddd DD/MM/YY", "date.short": "DD/MM/YY", - "date.medium": "DD/MMM/YYYY", + "date.medium": "DD/MM/YYYY", "date.long": "ddd DD MMM YYYY", "date.full": "dddd DD MMMM YYYY", "time.short": "HH:mm", @@ -57,7 +61,7 @@ "time.long": "HH:mm:ss", "time.full": "HH:mm:ss.SSS", "datetime.short": "DD/MM/YY HH:mm", - "datetime.medium": "DD/MMM/YYYY HH:mm", + "datetime.medium": "DD/MM/YYYY HH:mm", "datetime.long": "ddd DD MMM YYYY HH:mm", "datetime.full": "dddd DD MMMM YYYY HH:mm" } diff --git a/packages/core/init/data/core_User.json b/packages/core/init/data/core_User.json index 34b0720e6..ed2ac1fc9 100644 --- a/packages/core/init/data/core_User.json +++ b/packages/core/init/data/core_User.json @@ -5,20 +5,20 @@ "data": [ { "id": 1, - "login": "root@host.local", + "login": "root@equal.local", "password": "secure_password", - "firstname": "root", - "lastname": "@system", + "firstname": "Root", + "lastname": "USER", "language": "en", "validated": true, "groups_ids": [1, 2] }, { "id": 2, - "login": "cedric@equal.run", + "login": "user@equal.local", "password": "safe_pass", - "firstname": "Cédric", - "lastname": "Françoys", + "firstname": "First", + "lastname": "USER", "language": "en", "validated": true, "groups_ids": [2] diff --git a/packages/core/manifest.json b/packages/core/manifest.json index f3d2d2050..70bab8f2c 100644 --- a/packages/core/manifest.json +++ b/packages/core/manifest.json @@ -1,10 +1,15 @@ { "name": "core", - "description": "Foundations package holding the application logic of the elementary entities.", + "description": "Foundation package holding common application logic and elementary entities.", "version": "2.0", "authors": ["Cedric Francoys"], "license": "LGPL-3", + "tags": [ "equal", "core" ], "depends_on": [], - "apps": [ "apps", "auth", "app", "settings","workbench" ], - "tags": ["equal", "core"] -} \ No newline at end of file + "requires": { + "swiftmailer/swiftmailer": "^6.2", + "phpoffice/phpspreadsheet": "^1.4", + "dompdf/dompdf": "^0.8.3" + }, + "apps": [ "apps", "auth", "app", "settings", "workbench", "welcome" ] +} diff --git a/packages/core/tests/access.php b/packages/core/tests/access.php index 433a46ef5..297a6b0e4 100644 --- a/packages/core/tests/access.php +++ b/packages/core/tests/access.php @@ -1,6 +1,6 @@ + This file is part of the eQual framework Some Rights Reserved, Cedric Francoys, 2010-2021 Licensed under GNU GPL 3 license */ @@ -58,7 +58,7 @@ // groups assignments '0201' => [ - 'description' => "Verify assignment with non-existing group.", + 'description' => "Verify assignment with non-existing group.", 'help' => "Create a user, check with a non existing group.", 'return' => ['integer'], 'arrange' => function() use($providers) { @@ -82,7 +82,7 @@ 'help' => "Create a user, check with a non existing group.", 'return' => ['integer'], 'arrange' => function() use($providers) { - $user = User::create(['login' => 'user_test_1@example.com', 'password' => 'abcd1234'])->first(); + $user = User::create(['login' => 'user_test_2@example.com', 'password' => 'abcd1234'])->first(); return $user['id']; }, 'act' => function($user_id) use($providers) { @@ -93,7 +93,7 @@ return ($access->hasGroup('users', $user_id) && !$access->hasGroup('foo_non_existing_group', $user_id)); }, 'rollback' => function() { - User::search(['login', '=', 'user_test_1@example.com'])->delete(true); + User::search(['login', '=', 'user_test_2@example.com'])->delete(true); } ], @@ -102,7 +102,7 @@ 'help' => "Create a user, assign it to a group, and check the group membership of the user.", 'return' => ['array'], 'arrange' => function() use($providers) { - $user = User::create(['login' => 'user_test_1@example.com', 'password' => 'abcd1234'])->first(); + $user = User::create(['login' => 'user_test_3@example.com', 'password' => 'abcd1234'])->first(); $group = Group::create(['name' => 'test1'])->first(); return [$user['id'], $group['id']]; }, @@ -120,7 +120,7 @@ }, 'rollback' => function() { Group::search(['name', '=', 'test1'])->delete(true); - User::search(['login', '=', 'user_test_1@example.com'])->delete(true); + User::search(['login', '=', 'user_test_3@example.com'])->delete(true); } ], @@ -155,7 +155,7 @@ 'description' => "Check if a user has a right on all objects.", 'help' => "Create a user, assign it to a group, grant some rights to that group and check the resulting rights of the user.", 'arrange' => function() use($providers) { - $user = User::create(['login' => 'user_test_1@example.com', 'password' => 'abcd1234'])->first(); + $user = User::create(['login' => 'user_test_4@example.com', 'password' => 'abcd1234'])->first(); return $user['id']; }, 'assert' => function($user_id) use($providers) { @@ -163,7 +163,7 @@ return !$access->hasRight($user_id, EQ_R_MANAGE, 'core\User'); }, 'rollback' => function() { - User::search(['login', '=', 'user_test_1@example.com'])->delete(true); + User::search(['login', '=', 'user_test_4@example.com'])->delete(true); } ], @@ -172,8 +172,8 @@ 'help' => "Create a user, assign it to a group, grant some rights to that group and check the resulting rights of the user.", 'arrange' => function() use($providers) { $access = $providers['access']; - $user = User::create(['login' => 'user_test_1@example.com', 'password' => 'abcd1234'])->first(); - $group = Group::create(['name' => 'test1'])->first(); + $user = User::create(['login' => 'user_test_5@example.com', 'password' => 'abcd1234'])->first(); + $group = Group::create(['name' => 'test2'])->first(); $access->addGroup($group['id'], $user['id']); return $group['id']; }, @@ -188,10 +188,9 @@ return $access->hasRight($user['id'], EQ_R_READ|EQ_R_WRITE|EQ_R_MANAGE, '*'); }, 'rollback' => function() { - Group::search(['name', '=', 'test1'])->delete(true); - User::search(['login', '=', 'user_test_1@example.com'])->delete(true); + Group::search(['name', '=', 'test2'])->delete(true); + User::search(['login', '=', 'user_test_5@example.com'])->delete(true); } - ], '0305' => [ @@ -199,8 +198,8 @@ 'help' => "Create a user, assign it to a group, grant some rights to that group, then remove back those rights, and check the resulting rights of the user.", 'arrange' => function() use($providers) { $access = $providers['access']; - $user = User::create(['login' => 'user_test_1@example.com', 'password' => 'abcd1234'])->first(); - $group = Group::create(['name' => 'test1'])->first(); + $user = User::create(['login' => 'user_test_6@example.com', 'password' => 'abcd1234'])->first(); + $group = Group::create(['name' => 'test3'])->first(); $access->grantGroups($group['id'], EQ_R_MANAGE, '*'); $access->addGroup($group['id'], $user['id']); return [$user['id'], $group['id']]; @@ -217,8 +216,8 @@ return !$access->hasRight($user_id, EQ_R_MANAGE, 'core\User', 1); }, 'rollback' => function() { - Group::search(['name', '=', 'test1'])->delete(true); - User::search(['login', '=', 'user_test_1@example.com'])->delete(true); + Group::search(['name', '=', 'test3'])->delete(true); + User::search(['login', '=', 'user_test_6@example.com'])->delete(true); } ], @@ -227,7 +226,7 @@ 'description' => "Check if an action is authorized.", 'help' => "Create a user, and check if the user can perform an action on its own object.", 'arrange' => function() use($providers) { - $user = User::create(['login' => 'user_test_1@example.com', 'password' => 'abcd1234'])->first(); + $user = User::create(['login' => 'user_test_7@example.com', 'password' => 'abcd1234'])->first(); return $user['id']; }, 'assert' => function($user_id) use($providers) { @@ -235,7 +234,7 @@ return !boolval(count($access->canPerform($user_id, 'validate', 'core\User', $user_id))); }, 'rollback' => function() { - User::search(['login', '=', 'user_test_1@example.com'])->delete(true); + User::search(['login', '=', 'user_test_7@example.com'])->delete(true); } ] ]; diff --git a/packages/core/tests/adapters.php b/packages/core/tests/adapters.php index 0b4231d28..3b084213e 100644 --- a/packages/core/tests/adapters.php +++ b/packages/core/tests/adapters.php @@ -1,6 +1,6 @@ + This file is part of the eQual framework Some Rights Reserved, Cedric Francoys, 2010-2021 Licensed under GNU GPL 3 license */ diff --git a/packages/core/tests/auth.php b/packages/core/tests/auth.php index 040944d41..10fafe190 100644 --- a/packages/core/tests/auth.php +++ b/packages/core/tests/auth.php @@ -1,6 +1,6 @@ + This file is part of the eQual framework Some Rights Reserved, Cedric Francoys, 2010-2021 Licensed under GNU GPL 3 license */ diff --git a/packages/core/tests/collections.php b/packages/core/tests/collections.php new file mode 100644 index 000000000..55eaf1e75 --- /dev/null +++ b/packages/core/tests/collections.php @@ -0,0 +1,44 @@ + + Some Rights Reserved, Cedric Francoys, 2010-2021 + Licensed under GNU GPL 3 license +*/ +use equal\orm\ObjectManager; +use equal\http\HttpRequest; +use core\User; +use core\Group; + +$providers = eQual::inject(['context', 'orm', 'auth', 'access']); + +$tests = [ + '40101' => [ + 'description' => "Retrieve sub-object using dot notation with ORM::read (with recursion).", + 'act' => function () use ($providers) { + $orm = $providers['orm']; + $res = $orm->read('core\User', QN_ROOT_USER_ID, ['name', 'groups_ids.name', 'groups_ids.id', 'groups_ids.users_ids.name']); + return ($res > 0 && count($res))?reset($res):[]; + }, + 'assert' => function($result) { + $res = []; + foreach($result['groups_ids.name'] as $gid => $group) { + if(!isset($res[$gid])) { + $res[$gid] = []; + } + $res[$gid]['name'] = $group['name']; + } + foreach($result['groups_ids.users_ids.name'] as $gid => $group) { + if(!isset($res[$gid])) { + $res[$gid] = []; + } + foreach($group['users_ids.name'] as $uid => $user) { + if(!isset($res[$gid]['users_ids'])) { + $res[$gid]['users_ids'] = []; + } + $res[$gid]['users_ids'][$uid] = $user['name']; + } + } + return ($res[1]['name'] == 'admins' && $res[2]['users_ids'][1] == 'root@equal.local'); + } + ] +]; \ No newline at end of file diff --git a/packages/core/tests/computed.php b/packages/core/tests/computed.php new file mode 100644 index 000000000..cb6acac84 --- /dev/null +++ b/packages/core/tests/computed.php @@ -0,0 +1,27 @@ + + Some Rights Reserved, Cedric Francoys, 2010-2021 + Licensed under GNU GPL 3 license +*/ + +$tests = [ + + '1001' => [ + 'description' => "Checking dependents chaining with computed field of related object.", + 'act' => function () { + $result = []; + $test = core\test\Test::create(['string_short' => 'test 0'])->read(['id'])->first(); + $test1 = core\test\Test1::create(['test_id' => $test['id']])->read(['id', 'test'])->first(); + $result[] = $test1['test']; + core\test\Test::id($test['id'])->update(['string_short' => 'test 1']); + $test1 = core\test\Test1::id($test1['id'])->read(['test'])->first(); + $result[] = $test1['test']; + return $result; + }, + 'assert' => function($result) { + return ($result[0] == 'test 0' && $result[1] == 'test 1'); + } + ] + +]; \ No newline at end of file diff --git a/packages/core/tests/cron.php b/packages/core/tests/cron.php index fa16362d7..b5a58f068 100644 --- a/packages/core/tests/cron.php +++ b/packages/core/tests/cron.php @@ -1,6 +1,6 @@ + This file is part of the eQual framework Some Rights Reserved, Cedric Francoys, 2010-2021 Licensed under GNU GPL 3 license */ diff --git a/packages/core/tests/demo.php b/packages/core/tests/demo.php index 958e904be..19399c863 100644 --- a/packages/core/tests/demo.php +++ b/packages/core/tests/demo.php @@ -1,17 +1,15 @@ + This file is part of the eQual framework Some Rights Reserved, Cedric Francoys, 2010-2021 Licensed under GNU GPL 3 license */ /** - * A global var `$test` is expected to be set by each tests set (that var is used in the `core_package_test` controller). - * As the current file is injected in the global scope, this line is not mandatory and is left in order to ease the understanding. - * - * @var array $test Global var holding the test descriptors. + * A `$test` var is expected to be set by each tests set. + * That var is used in the `core_test_package` controller and, since tests sets are loaded using `include($filename)`, + * the `$tests` var is shared between tests sets and the parent script. */ -global $test; $tests = [ // Each key of the associative array is a test identifier that maps to a test descriptor. diff --git a/packages/core/tests/http.php b/packages/core/tests/http.php index c7f1a151e..dec5d6239 100644 --- a/packages/core/tests/http.php +++ b/packages/core/tests/http.php @@ -1,6 +1,6 @@ + This file is part of the eQual framework Some Rights Reserved, Cedric Francoys, 2010-2021 Licensed under GNU GPL 3 license */ @@ -23,7 +23,7 @@ try { $request = new HttpRequest("http://localhost/me"); $response = $request - ->header('Authorization', 'Basic '.base64_encode("cedric@equal.run:safe_pass")) + ->header('Authorization', 'Basic '.base64_encode("user@equal.local:safe_pass")) ->send(); return $response->body(); } @@ -33,7 +33,7 @@ } return $values; }, - 'expected' => ['id' => 2, 'login' => 'cedric@equal.run', 'firstname' => 'Cédric', 'lastname' => 'FRANÇOYS', 'language' => 'fr'] + 'expected' => ['id' => 2, 'login' => 'user@equal.local', 'firstname' => 'User', 'lastname' => 'USER', 'language' => 'fr'] ), */ ]; \ No newline at end of file diff --git a/packages/core/tests/orm.php b/packages/core/tests/orm.php index cb11cef09..3a5fbd9b1 100644 --- a/packages/core/tests/orm.php +++ b/packages/core/tests/orm.php @@ -1,6 +1,6 @@ + This file is part of the eQual framework Some Rights Reserved, Cedric Francoys, 2010-2021 Licensed under GNU GPL 3 license */ @@ -17,7 +17,7 @@ //1xxx : calls related to the ObjectManger instance '1000' => array( - 'description' => "Get instance of the object Manager", + 'description' => "Get the instance of the object Manager.", 'return' => array('boolean'), 'expected' => true, 'test' => function (){ @@ -27,7 +27,7 @@ ), '1100' => array( - 'description' => "Check uniqueness of ObjectManager instance", + 'description' => "Check uniqueness of ObjectManager instance.", 'return' => array('boolean'), 'expected' => true, 'test' => function (){ @@ -42,13 +42,13 @@ // @return mixed (int or array) error code OR resulting associative array '2100' => array( - 'description' => "Requesting User object by passing an array holding a unique id", + 'description' => "Requesting User object by passing an array holding a unique id.", 'return' => array('integer', 'array'), 'expected' => array( '1' => array( 'language' => 'en', - 'firstname' => 'root', - 'lastname' => '@system' + 'firstname' => 'Root', + 'lastname' => 'USER' ) ), 'test' => function (){ @@ -63,13 +63,13 @@ ), '2101' => array( - 'description' => "Requesting User object by passing an integer as id", + 'description' => "Requesting User object by passing an integer as id.", 'return' => array('integer', 'array'), 'expected' => array( '1' => array( 'language' => 'en', - 'firstname' => 'root', - 'lastname' => '@system' + 'firstname' => 'Root', + 'lastname' => 'USER' ) ), 'test' => function (){ @@ -83,13 +83,13 @@ } ), '2102' => array( - 'description' => "Requesting User object by passing a string as id", + 'description' => "Requesting User object by passing a string as id.", 'return' => array('integer', 'array'), 'expected' => array( '1' => array( 'language' => 'en', - 'firstname' => 'root', - 'lastname' => '@system' + 'firstname' => 'Root', + 'lastname' => 'USER' ) ), 'test' => function (){ @@ -104,7 +104,7 @@ ), '2103' => array( - 'description' => "Requesting User object by giving a non-existing integer id", + 'description' => "Requesting User object by giving a non-existing integer id.", 'return' => array('integer', 'array'), 'expected' => array(), 'test' => function (){ @@ -114,13 +114,13 @@ ), '2104' => array( - 'description' => "Requesting User object by passing an array containing an invalid id", + 'description' => "Requesting User object by passing an array containing an invalid id.", 'return' => array('integer', 'array'), 'expected' => array( '1' => [ 'language' => 'en', - 'firstname' => 'root', - 'lastname' => '@system' + 'firstname' => 'Root', + 'lastname' => 'USER' ] ), 'test' => function () { @@ -131,12 +131,11 @@ $res[$oid] = $object->toArray(); } return $res; - } ), '2105' => array( - 'description' => "Call ObjectManager::read with empty value for \$ids parameter : empty array", + 'description' => "Call ObjectManager::read with empty value for \$ids parameter : empty array.", 'return' => array('integer', 'array'), 'expected' => array(), 'test' => function () { @@ -146,7 +145,7 @@ ), '2110' => array( - 'description' => "Call ObjectManager::read with missing \$ids parameters", + 'description' => "Call ObjectManager::read with missing \$ids parameters.", 'return' => array('integer', 'array'), 'expected' => [], 'test' => function () { @@ -155,7 +154,7 @@ } ), '2120' => array( - 'description' => "Call ObjectManager::read with wrong \$ids parameters", + 'description' => "Call ObjectManager::read with wrong \$ids parameters.", 'return' => array('integer', 'array'), 'expected' => array(), 'test' => function () { @@ -164,7 +163,7 @@ } ), '2130' => array( - 'description' => "Call ObjectManager::read some non-existing object from non-existing class", + 'description' => "Call ObjectManager::read some non-existing object from non-existing class.", 'return' => array('integer', 'array'), 'expected' => QN_ERROR_UNKNOWN_OBJECT, 'test' => function () { @@ -174,11 +173,11 @@ ), '2140' => array( - 'description' => "Call ObjectManager::read with a string as field", + 'description' => "Call ObjectManager::read with a string as field.", 'return' => array('integer', 'array'), 'expected' => array( '1' => array( - 'firstname' => 'root' + 'firstname' => 'Root' ) ), 'test' => function () { @@ -192,7 +191,7 @@ } ), '2150' => array( - 'description' => "Call ObjectManager::read with wrong \$fields value : non-existing field name", + 'description' => "Call ObjectManager::read with wrong \$fields value : non-existing field name.", 'return' => array('integer', 'array'), 'expected' => [ '1' => [] @@ -208,9 +207,9 @@ } ), '2151' => array( - 'description' => "Call ObjectManager::read with wrong \$fields value : non-existing field name", + 'description' => "Call ObjectManager::read with wrong \$fields value : non-existing field name.", 'return' => array('integer', 'array'), - 'expected' => array('1' => array('firstname' => 'root') ), + 'expected' => array('1' => array('firstname' => 'Root') ), 'test' => function () { $res = []; $om = ObjectManager::getInstance(); @@ -225,7 +224,7 @@ //22xx : calls related to the create method '2210' => array( - 'description' => "Create a user (no validation)", + 'description' => "Create a user (no validation).", 'return' => array('integer'), 'test' => function () { global $dummy_user_id; @@ -241,7 +240,7 @@ ), '2220' => [ - 'description' => "Create a group (no validation)", + 'description' => "Create a group (no validation).", 'return' => array('integer'), 'act' => function () { $om = ObjectManager::getInstance(); @@ -263,7 +262,7 @@ //24xx : calls related to the remove method '2401' => array( - 'description' => "Remove a user (no validation)", + 'description' => "Remove a user (no validation).", 'return' => array('integer', 'array'), 'assert' => function($result) { return ($result > 0); @@ -279,7 +278,7 @@ // @signature : public function search($object_class, $domain=NULL, $order='id', $sort='asc', $start='0', $limit='0', $lang='en') { // @return : mixed (integer or array) '2501' => array( - 'description' => "Search an object with valid clause 'ilike'", + 'description' => "Search an object with valid clause 'ilike'.", 'return' => array('integer', 'array'), 'expected' => array('2'), 'test' => function () { @@ -288,7 +287,7 @@ } ), '2502' => array( - 'description' => "Search an object with invalid clause 'ilike' (non-existing field)", + 'description' => "Search an object with invalid clause 'ilike' (non-existing field).", 'return' => array('integer', 'array'), 'expected' => QN_ERROR_INVALID_PARAM, 'test' => function () { @@ -297,7 +296,7 @@ } ), '2510' => array( - 'description' => "Search for some object : clause 'contains' on one2many field", + 'description' => "Search for some object : clause 'contains' on one2many field.", 'return' => array('boolean'), 'expected' => true, 'test' => function (){ @@ -306,7 +305,7 @@ }, ), '2520' => array( - 'description' => "Search for some object : clause 'contains' on one2many field (using a foreign key different from 'id')", + 'description' => "Search for some object : clause 'contains' on one2many field (using a foreign key different from 'id').", 'return' => array('boolean'), 'expected' => true, 'test' => function () { @@ -315,7 +314,7 @@ } ), '2530' => array( - 'description' => "Search for some object : clause 'contains' on many2one field", + 'description' => "Search for some object : clause 'contains' on many2one field.", 'return' => array('boolean'), 'expected' => true, 'test' => function () { @@ -331,7 +330,7 @@ 'expected' => QN_ROOT_USER_ID, 'test' => function () use($providers) { try { - $providers['auth']->authenticate('root@host.local', 'secure_password'); + $providers['auth']->authenticate('root@equal.local', 'secure_password'); $values = $providers['auth']->userId(); } catch(Exception $e) { @@ -343,11 +342,11 @@ ), '2620' => array( - 'description' => "Search for some object : clause 'contains' on many2many field", + 'description' => "Search for some object : clause 'contains' on many2many field.", 'return' => array('integer', 'array'), 'arrange' => function () use($providers) { try { - $providers['auth']->authenticate('cedric@equal.run', 'safe_pass'); + $providers['auth']->authenticate('user@equal.local', 'safe_pass'); // grant READ operation on all classes $providers['access']->grant(QN_R_READ); @@ -363,14 +362,14 @@ }, 'assert' => function($result) { return is_array($result) && count($result) == 2 && ( - count(array_diff(['id' => 1, 'login' => 'root@host.local'], (array) $result['1'])) == 0 - && count(array_diff(['id' => 2, 'login' => 'cedric@equal.run'], (array) $result['2'])) == 0 + count(array_diff(['id' => 1, 'login' => 'root@equal.local'], (array) $result['1'])) == 0 + && count(array_diff(['id' => 2, 'login' => 'user@equal.local'], (array) $result['2'])) == 0 ); } ), '2631' => array( - 'description' => "Add a user to a given group", + 'description' => "Add a user to a given group.", 'return' => array('integer', 'array'), 'act' => function () use($providers) { try { @@ -389,7 +388,7 @@ }, 'assert' => function($result) { return ( - count(array_diff(['id' => 1, 'login' => 'root@host.local'], (array) $result['1'])) == 0 + count(array_diff(['id' => 1, 'login' => 'root@equal.local'], (array) $result['1'])) == 0 ); } ), @@ -416,7 +415,7 @@ 'return' => array('integer', 'array'), 'act' => function () { try { - $values = User::search(['login', 'like', 'cedric@equal.run']) + $values = User::search(['login', 'like', 'user@equal.local']) ->read(['login']) ->get(); } @@ -429,7 +428,7 @@ 'assert' => function($result) { return ( count($result) && - count(array_diff(['id' => 2, 'login' => 'cedric@equal.run'], (array) $result[2])) == 0 + count(array_diff(['id' => 2, 'login' => 'user@equal.local'], (array) $result[2])) == 0 ); } ), @@ -438,7 +437,7 @@ 'return' => array('integer', 'array'), 'act' => function () { try { - $values = User::search(['login', '=', 'cedric@equal.run']) + $values = User::search(['login', '=', 'user@equal.local']) ->read(['login']) ->get(true); } @@ -451,7 +450,7 @@ 'assert' => function($result) { return ( count($result) && - count(array_diff(['id' => 2, 'login' => 'cedric@equal.run'], (array) $result[0])) == 0 + count(array_diff(['id' => 2, 'login' => 'user@equal.local'], (array) $result[0])) == 0 ); } ), diff --git a/packages/core/tests/validation.php b/packages/core/tests/validation.php new file mode 100644 index 000000000..b54d3647b --- /dev/null +++ b/packages/core/tests/validation.php @@ -0,0 +1,210 @@ + + Some Rights Reserved, Cedric Francoys, 2010-2021 + Licensed under GNU GPL 3 license +*/ +use core\User; + +$tests = [ + + '1001' => [ + 'description' => "Checking `User::getConstraints()` 'username' validation rule with invalid value.", + 'act' => function () { + $result = 0; + try { + User::id(1)->update(['username' => '-invalid_name-']); + } + catch(Exception $e) { + $result = $e->getCode(); + } + return $result; + }, + 'assert' => function($result) { + return ($result == EQ_ERROR_INVALID_PARAM); + } + ], + '1002' => [ + 'description' => "Checking `User::getConstraints()` 'username' validation rule with valid value.", + 'act' => function () { + $result = 0; + try { + User::id(1)->update(['username' => 'valid-name']); + } + catch(Exception $e) { + $result = $e->getCode(); + } + return $result; + }, + 'assert' => function($result) { + return ($result == 0); + }, + 'rollback' => function() { + User::id(1)->update(['username' => null]); + } + ], + '1011' => [ + 'description' => "Checking usage validation 'text/plain:9' with valid value.", + 'act' => function () { + $result = 0; + try { + core\test\Test::create(['string_short' => '123456789']); + } + catch(Exception $e) { + $result = $e->getCode(); + } + return $result; + }, + 'assert' => function($result) { + return ($result == 0); + } + ], + '1012' => [ + 'description' => "Checking usage validation 'text/plain:9' with invalid (size overflow).", + 'act' => function () { + $result = 0; + try { + core\test\Test::create(['string_short' => '0123456789']); + } + catch(Exception $e) { + $result = $e->getCode(); + } + return $result; + }, + 'assert' => function($result) { + return ($result == EQ_ERROR_INVALID_PARAM); + } + ], + '1021' => [ + 'description' => "Checking usage validation 'amount/money' with invalid (not a number).", + 'act' => function () { + $result = 0; + try { + core\test\Test::create(['float_amount' => 'abc']); + } + catch(Exception $e) { + $result = $e->getCode(); + } + return $result; + }, + 'assert' => function($result) { + return ($result == EQ_ERROR_INVALID_PARAM); + } + ], + '1022' => [ + 'description' => "Checking usage validation 'amount/money' with invalid (string).", + 'act' => function () { + $result = 0; + try { + core\test\Test::create(['float_amount' => '123,456']); + } + catch(Exception $e) { + $result = $e->getCode(); + } + return $result; + }, + 'assert' => function($result) { + return ($result == EQ_ERROR_INVALID_PARAM); + } + ], + '1023' => [ + 'description' => "Checking usage validation 'amount/money' with invalid (decimal digits overflow).", + 'act' => function () { + $result = 0; + try { + core\test\Test::create(['float_amount' => 123.456789]); + } + catch(Exception $e) { + $result = $e->getCode(); + } + return $result; + }, + 'assert' => function($result) { + return ($result == EQ_ERROR_INVALID_PARAM); + } + ], + '1024' => [ + 'description' => "Checking usage validation 'amount/money' with valid.", + 'act' => function () { + $result = 0; + try { + core\test\Test::create(['float_amount' => 123.4567]); + } + catch(Exception $e) { + $result = $e->getCode(); + } + return $result; + }, + 'assert' => function($result) { + return ($result == 0); + } + ], + '1031' => [ + 'description' => "Checking usage validation 'currency' with invalid.", + 'act' => function () { + $result = 0; + try { + core\test\Test::create(['string_currency' => 'tralala']); + } + catch(Exception $e) { + $result = $e->getCode(); + } + return $result; + }, + 'assert' => function($result) { + return ($result == EQ_ERROR_INVALID_PARAM); + } + ], + '1032' => [ + 'description' => "Checking usage validation 'currency' with valid.", + 'act' => function () { + $result = 0; + try { + core\test\Test::create(['string_currency' => 'USD']); + } + catch(Exception $e) { + $result = $e->getCode(); + } + return $result; + }, + 'assert' => function($result) { + return ($result == 0); + } + ], + '1041' => [ + 'description' => "Checking usage validation 'datetime' with invalid (string).", + 'act' => function () { + $result = 0; + try { + core\test\Test::create(['datetime' => 'foo']); + } + catch(Exception $e) { + $result = $e->getCode(); + } + return $result; + }, + 'assert' => function($result) { + return ($result == EQ_ERROR_INVALID_PARAM); + } + ], + '1042' => [ + 'description' => "Checking usage validation 'datetime' with valid.", + 'act' => function () { + $result = 0; + try { + core\test\Test::create(['datetime' => 1711990542]); + } + catch(Exception $e) { + $result = $e->getCode(); + } + return $result; + }, + 'assert' => function($result) { + return ($result == 0); + } + ] + + + + +]; \ No newline at end of file diff --git a/packages/core/uml/overview.or.equml b/packages/core/uml/overview.erd.json similarity index 100% rename from packages/core/uml/overview.or.equml rename to packages/core/uml/overview.erd.json diff --git a/packages/core/views/User.form.default.json b/packages/core/views/User.form.default.json index e64259ea0..64fab218c 100644 --- a/packages/core/views/User.form.default.json +++ b/packages/core/views/User.form.default.json @@ -1,6 +1,6 @@ { "name": "User", - "description": "Simple form for displaying User", + "description": "Basic form for displaying User.", "layout": { "groups": [ { @@ -8,7 +8,7 @@ "sections": [ { "label": "User details", - "id": "user_details", + "id": "section.details", "rows": [ { "columns": [ @@ -17,6 +17,7 @@ "align": "left", "items": [ { + "id": "item.user_id", "type": "field", "value": "id", "width": "50%", diff --git a/packages/core/views/User.list.default.json b/packages/core/views/User.list.default.json index 74d74e503..a17eb30c9 100644 --- a/packages/core/views/User.list.default.json +++ b/packages/core/views/User.list.default.json @@ -46,16 +46,6 @@ "readonly": true } }, - { - "type": "field", - "value": "firstname", - "width": "20%" - }, - { - "type": "field", - "value": "lastname", - "width": "20%" - }, { "type": "field", "value": "language", diff --git a/packages/core/views/menu.settings.left.json b/packages/core/views/menu.settings.left.json index 83f6bbdec..d334e3003 100644 --- a/packages/core/views/menu.settings.left.json +++ b/packages/core/views/menu.settings.left.json @@ -3,6 +3,7 @@ "access": { "groups": ["setting.default.user"] }, + "search": true, "layout": { "items": [ { diff --git a/public/console.php b/public/console.php index 134879f48..bec2a3d33 100644 --- a/public/console.php +++ b/public/console.php @@ -4,415 +4,683 @@ Some Rights Reserved, Cedric Francoys, 2010-2023 Licensed under GNU LGPL 3 license */ - -define('LOG_FILE_NAME', 'eq_error.log'); -$data = ''; +error_reporting(0); // get log file, using variation from URL, if any -$log_file = LOG_FILE_NAME.( (isset($_GET['f']) && strlen($_GET['f']))?('.'.$_GET['f']):''); - +$log_file = (isset($_GET['f']) && strlen($_GET['f']))?$_GET['f']:'eq_error.log'; // retrieve logs history (variations on filename) $log_variations = []; -foreach(glob('../log/'.LOG_FILE_NAME.'.*') as $file) { +foreach(glob('../log/*.log') as $file) { $log_variations[] = pathinfo($file, PATHINFO_EXTENSION); } -// get query from URL, if any -$query = (isset($_GET['q']))?$_GET['q']:''; -// adapt params -if(isset($_GET['level']) && $_GET['level'] == '') { - unset($_GET['level']); -} -if(isset($_GET['mode']) && $_GET['mode'] == '') { - unset($_GET['mode']); -} -if(isset($_GET['date']) && $_GET['date'] == '') { - unset($_GET['date']); -} +// no param given : frond-end App provider +if(!count($_GET)) { + echo ' + + + + + + + - - - - -
Copied to clipboard
-