diff --git a/.gitignore b/.gitignore index 3c7be4fad..38b53edbf 100644 --- a/.gitignore +++ b/.gitignore @@ -20,5 +20,4 @@ public/* !public/index.php !public/console.php !public/assets -!public/console2.php public/assets/env/config.json diff --git a/.gitmodules b/.gitmodules index 702f0fa7c..341b91404 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,3 +4,18 @@ [submodule "packages/core/apps/workbench/source"] path = packages/core/apps/workbench/source url = https://github.com/equalframework/apps-core-workbench.git +[submodule "packages/core/apps/welcome/source"] + path = packages/core/apps/welcome/source + url = https://github.com/equalframework/apps-core-welcome.git +[submodule "packages/core/apps/apps/source"] + path = packages/core/apps/apps/source + url = https://github.com/equalframework/apps-core-apps.git +[submodule "packages/core/apps/settings/source"] + path = packages/core/apps/settings/source + url = https://github.com/equalframework/apps-core-settings.git +[submodule "packages/core/apps/auth/source"] + path = packages/core/apps/auth/source + url = https://github.com/equalframework/apps-core-auth.git +[submodule "packages/core/apps/app/source"] + path = packages/core/apps/app/source + url = https://github.com/equalframework/apps-core-app.git diff --git a/config/schema.json b/config/schema.json index 20e1109c9..65ac1bd12 100644 --- a/config/schema.json +++ b/config/schema.json @@ -23,12 +23,14 @@ }, "AUTH_ACCESS_TOKEN_VALIDITY": { "type": "integer", + "usage": "time/duration", "description": "Validity duration of the access token, in seconds.", "instant": true, "default": 3600 }, "AUTH_REFRESH_TOKEN_VALIDITY": { "type": "integer", + "usage": "time/duration", "default": "90d" }, "AUTH_TOKEN_HTTPS":{ @@ -254,6 +256,7 @@ }, "HTTP_REQUEST_TIMEOUT": { "type": "integer", + "usage": "time/duration", "description": "Maximum wait time, in seconds, before cancelling a pending HTTP request. Must not exceed `max_execution_time`.", "instant": true, "default": 10 @@ -283,19 +286,23 @@ }, "UPLOAD_MAX_FILE_SIZE": { "type": "integer", + "usage": "amount/data", "description": "Maximum authorized size for file upload (in bytes). Keep in mind that this parameter does not override the PHP 'upload_max_filesize' directive, o it can be more restrictive but will not be effective if set higher.", - "default": "64M", + "default": "64MB", "examples": [ 0, 32768, - "128M" + "128MB" ] }, "MEM_LIMIT": { "type": "integer", - "description": "Memory amount set as upper limit (task scheduler stops running tasks while above limit).", + "usage": "amount/data", + "description": "Upper limit of total usable memory (in bytes) available for the whole instance.", + "help": "This parameter is a threshold that the system tries not to exceed (the task scheduler stops running tasks while above limit). However, under certain circumstances, this limit may still be surpassed. Besides, if that value is above the memory_limit set in php.ini, it will have no effect.", "instant": true, "environment": "EQ_MEM_LIMIT", + "default": "512MB", "examples": [ "200MB" ] diff --git a/eq.lib.php b/eq.lib.php index 834d922cb..c53911a23 100644 --- a/eq.lib.php +++ b/eq.lib.php @@ -101,7 +101,6 @@ 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); @@ -132,7 +131,6 @@ define('EQ_R_DELETE', 8); define('EQ_R_MANAGE', 16); define('EQ_R_ALL', 31); - // equivalence map for constant names migration // #deprecated define('QN_R_CREATE', EQ_R_CREATE); @@ -147,12 +145,20 @@ * * Note : make sure that the ids in DB are set and matching these */ - define('QN_GUEST_USER_ID', 0); - define('QN_ROOT_USER_ID', 1); + define('EQ_GUEST_USER_ID', 0); + define('EQ_ROOT_USER_ID', 1); + // equivalence map for constant names migration + // #deprecated + define('QN_GUEST_USER_ID', EQ_GUEST_USER_ID); + define('QN_ROOT_USER_ID', EQ_ROOT_USER_ID); // default group (all users are members of the default group) - define('QN_ROOT_GROUP_ID', 1); - define('QN_DEFAULT_GROUP_ID', 2); + define('EQ_ROOT_GROUP_ID', 1); + define('EQ_DEFAULT_GROUP_ID', 2); + // equivalence map for constant names migration + // #deprecated + define('QN_ROOT_GROUP_ID', EQ_ROOT_GROUP_ID); + define('QN_DEFAULT_GROUP_ID', EQ_DEFAULT_GROUP_ID); /* @@ -329,7 +335,7 @@ function decrypt($string) { return $output; } - function strtoint($value) { + function strtoint($value, $usage = '') { if(is_string($value)) { if($value == 'null') { $value = null; @@ -349,6 +355,7 @@ function strtoint($value) { $value = intval($value); } elseif(is_scalar($value)) { + // fallback suffixes coefficients (defaults) $suffixes = [ 'B' => 1, 'KB' => 1024, @@ -362,6 +369,45 @@ function strtoint($value) { 'M' => 3600*24*30, 'Y' => 3600*24*365 ]; + // #todo - replicate this in DataAdapterJsonInteger + switch($usage) { + case 'amount/data': + $suffixes = [ + 'b' => 1, + 'B' => 1, + 'k' => 1000, + 'K' => 1000, + 'kb' => 1000, + 'KB' => 1000, + 'kib' => 1024, + 'KiB' => 1024, + 'm' => 1000000, + 'M' => 1000000, + 'mb' => 1000000, + 'MB' => 1000000, + 'mib' => 1048576, + 'MiB' => 1048576, + 'g' => 1000000000, + 'gb' => 1000000000, + 'gib' => 1073741824, + 'GiB' => 1073741824 + ]; + break; + case 'time/duration': + $suffixes = [ + 'ms' => 0.001, + 's' => 1, + 'm' => 60, + 'h' => 3600, + 'd' => 3600*24, + 'D' => 3600*24, + 'w' => 3600*24*7, + 'M' => 3600*24*30, + 'y' => 3600*24*365, + 'Y' => 3600*24*365 + ]; + break; + } $val = (string) $value; $intval = intval($val); foreach($suffixes as $suffix => $factor) { @@ -448,7 +494,7 @@ function export($property) { } } else { - $value = strtoint($value); + $value = strtoint($value, $constants_schema[$property]['usage'] ?? ''); } } // handle encrypted values @@ -646,6 +692,7 @@ public static function announce(array $announcement) { $items = explode(';', $part); $accepted[] = trim(reset($items)); } + // #todo - when should we implement strict check on accepted content type ? if(!in_array($announcement['response']['content-type'], $accepted) && !in_array('*/*', $accepted)) { /* // send "406 Not Acceptable" @@ -683,7 +730,7 @@ public static function announce(array $announcement) { // prevent requests from non-allowed origins (for non-https requests, this can be bypassed by manually setting requests header) if($origin != '*' && $origin != $request_origin) { // raise an exception with error details - throw new \Exception('origin_not_allowed', QN_ERROR_NOT_ALLOWED); + throw new \Exception('origin_not_allowed', EQ_ERROR_NOT_ALLOWED); } // set headers accordingly to response definition // #todo allow to customize (override) these values @@ -776,6 +823,10 @@ public static function announce(array $announcement) { list($headers, $result) = unserialize(file_get_contents($cache_filename)); // build response with cached headers foreach($headers as $header => $value) { + // discard unwanted headers + if(in_array($header, ['Set-Cookie', 'Refresh'])) { + continue; + } $response->header($header, $value); } $response @@ -796,19 +847,19 @@ public static function announce(array $announcement) { list($access, $auth) = $container->get(['access', 'auth']); if(isset($announcement['access']['visibility'])) { if($announcement['access']['visibility'] == 'private' && php_sapi_name() != 'cli') { - throw new \Exception('private_operation', QN_ERROR_NOT_ALLOWED); + throw new \Exception('private_operation', EQ_ERROR_NOT_ALLOWED); } if($announcement['access']['visibility'] == 'protected') { // #memo - regular rules will apply (non identified user shouldn't be granted unless DEFAULT_RIGHTS allow it) if($auth->userId() <= 0) { - throw new \Exception('protected_operation', QN_ERROR_NOT_ALLOWED); + throw new \Exception('protected_operation', EQ_ERROR_NOT_ALLOWED); } } } if(isset($announcement['access']['users'])) { // disjunctions on users $current_user_id = $auth->userId(); - if($current_user_id != QN_ROOT_USER_ID) { + if($current_user_id != EQ_ROOT_USER_ID) { // #todo - add support for checks on login $allowed = false; $users = (array) $announcement['access']['users']; @@ -819,29 +870,35 @@ public static function announce(array $announcement) { } } if(!$allowed) { - throw new \Exception('restricted_operation', QN_ERROR_NOT_ALLOWED); + throw new \Exception('restricted_operation', EQ_ERROR_NOT_ALLOWED); } } } if(isset($announcement['access']['groups'])) { $current_user_id = $auth->userId(); - if($current_user_id != QN_ROOT_USER_ID) { - // disjunctions on groups - $allowed = false; + if($current_user_id != EQ_ROOT_USER_ID) { + $allowed = $access->hasGroup(EQ_ROOT_GROUP_ID); $groups = (array) $announcement['access']['groups']; foreach($groups as $group) { + if($allowed) { + break; + } if($access->hasGroup($group)) { $allowed = true; - break; } } if(!$allowed) { - throw new \Exception('restricted_operation', QN_ERROR_NOT_ALLOWED); + throw new \Exception('restricted_operation', EQ_ERROR_NOT_ALLOWED); } } } } + /** @var \equal\data\adapt\DataAdapterProvider */ + $dap = $container->get('adapt'); + // #todo - use the adapter based on content-type header, if any + /** @var \equal\data\adapt\DataAdapter */ + $adapter = $dap->get('json'); // 1) check if all required parameters have been received @@ -852,7 +909,7 @@ public static function announce(array $announcement) { $mandatory_params[] = $param; } } - // if at least one mandatory param is missing + // if at least one mandatory param is missing, reply with announcement $missing_params = array_values(array_diff($mandatory_params, array_keys($body))); if( count($missing_params) || isset($body['announce']) || $method == 'OPTIONS' ) { // #memo - we don't remove anything from the schema, so it can be returned as is for the UI @@ -897,7 +954,17 @@ public static function announce(array $announcement) { throw new \Exception('', 0); } // add announcement to response body + if(isset($announcement['params'])) { + // default values must be adapted to JSON + foreach((array) $announcement['params'] as $param => $config) { + if(isset($config['default'])) { + $f = new Field($config, $param); + $announcement['params'][$param]['default'] = $adapter->adaptOut($config['default'], $f->getUsage()); + } + } + } $response->body(['announcement' => $announcement]); + // if user asked for the announcement or browser requested fingerprint, set status and header accordingly if(isset($body['announce']) || $method == 'OPTIONS') { $response->status(200) @@ -913,7 +980,7 @@ public static function announce(array $announcement) { throw new \Exception('', 0); } // raise an exception with error details - throw new \Exception(implode(',', $missing_params), QN_ERROR_MISSING_PARAM); + throw new \Exception(implode(',', $missing_params), EQ_ERROR_MISSING_PARAM); } // 2) find any missing parameters @@ -929,11 +996,6 @@ public static function announce(array $announcement) { // 3) build result array and set default values for optional parameters that are missing and for which a default value is defined $invalid_params = []; - /** @var \equal\data\adapt\DataAdapterProvider */ - $dap = $container->get('adapt'); - // #todo - use the adapter based on content-type header, if any - /** @var \equal\data\adapt\DataAdapter */ - $adapter = $dap->get('json'); foreach($announcement['params'] as $param => $config) { // #memo - at some point condition had a clause "|| empty($body[$param])", remember not to alter received data! if(in_array($param, $missing_params) && isset($config['default'])) { @@ -993,7 +1055,7 @@ public static function announce(array $announcement) { $response->body(['announcement' => $announcement]); foreach($invalid_params as $invalid_param => $error_id) { // raise an exception with error details - throw new \Exception(serialize([$invalid_param => [$error_id => "Invalid value {$result[$invalid_param]} for parameter {$invalid_param}."]]), QN_ERROR_INVALID_PARAM); + throw new \Exception(serialize([$invalid_param => [$error_id => "Invalid value {$result[$invalid_param]} for parameter {$invalid_param}."]]), EQ_ERROR_INVALID_PARAM); } } @@ -1024,7 +1086,7 @@ public static function announce(array $announcement) { export($name, $value); } if(!\defined($name)) { - throw new \Exception("Requested constant {$name} is missing from configuration", QN_ERROR_INVALID_CONFIG); + throw new \Exception("Requested constant {$name} is missing from configuration", EQ_ERROR_INVALID_CONFIG); } } } @@ -1160,7 +1222,7 @@ public static function run($type, $operation, $body=[], $root=false) { if(is_null($resolved['type'])) { if(is_null($resolved['package'])) { // send 404 HTTP response - throw new \Exception("no_default_package", QN_ERROR_UNKNOWN_OBJECT); + throw new \Exception("no_default_package", EQ_ERROR_UNKNOWN_OBJECT); } if(defined('DEFAULT_APP')) { $resolved['type'] = 'show'; @@ -1169,7 +1231,7 @@ public static function run($type, $operation, $body=[], $root=false) { $request->uri()->set('show', $resolved['package'].'_'.constant('DEFAULT_APP')); } else { - throw new \Exception("No default app for package {$resolved['package']}", QN_ERROR_UNKNOWN_OBJECT); + throw new \Exception("No default app for package {$resolved['package']}", EQ_ERROR_UNKNOWN_OBJECT); } } // include resolved script, if any @@ -1192,7 +1254,7 @@ public static function run($type, $operation, $body=[], $root=false) { if(!is_file($filename)) { $filename = QN_BASEDIR.'/packages/core/'.$operation_conf['dir'].'/'.$resolved['package'].'.php'; if(!is_file($filename)) { - throw new \Exception("Unknown {$operation_conf['kind']} ({$resolved['type']}) {$resolved['operation']} ({$resolved['script']})", QN_ERROR_UNKNOWN_OBJECT); + throw new \Exception("Unknown {$operation_conf['kind']} ({$resolved['type']}) {$resolved['operation']} ({$resolved['script']})", EQ_ERROR_UNKNOWN_OBJECT); } } } diff --git a/lib/equal/cron/Scheduler.class.php b/lib/equal/cron/Scheduler.class.php index 779635b70..520112899 100644 --- a/lib/equal/cron/Scheduler.class.php +++ b/lib/equal/cron/Scheduler.class.php @@ -51,6 +51,10 @@ public function run($tasks_ids=[]) { } if($selected_tasks_ids > 0 && count($selected_tasks_ids)) { + + // #todo - do not try to run the task if current memory use is above MEM_LIMIT + // if(total_machine_mem_use >= constant('MEM_LIMIT')) { } + // if an exclusive task is already running, ignore current batch $running_tasks_ids = $orm->search('core\Task', [['status', '=', 'running'], ['is_exclusive', '=', true]]); if($running_tasks_ids > 0 && count($running_tasks_ids)) { @@ -110,7 +114,7 @@ public function run($tasks_ids=[]) { // run the task $data = \eQual::run('do', $task['controller'], $body, true); $status = 'success'; - $log = json_encode($data, JSON_PRETTY_PRINT); + $log = (string) json_encode($data, JSON_PRETTY_PRINT); } catch(\Exception $e) { // error occurred during execution diff --git a/lib/equal/data/adapt/adapters/json/DataAdapterJsonDateTime.class.php b/lib/equal/data/adapt/adapters/json/DataAdapterJsonDateTime.class.php index b74dd6a86..6417a1948 100644 --- a/lib/equal/data/adapt/adapters/json/DataAdapterJsonDateTime.class.php +++ b/lib/equal/data/adapt/adapters/json/DataAdapterJsonDateTime.class.php @@ -25,16 +25,18 @@ public function getType() { */ public function adaptIn($value, $usage, $locale='en') { $result = null; - if(is_numeric($value)) { - // value is a timestamp, keep it - $result = intval($value); - } - else { - // convert ISO 8601 to timestamp - $result = strtotime($value); - } - if(!$result) { - $result = null; + if(!is_null($value)) { + if(is_numeric($value)) { + // value is a timestamp, keep it + $result = intval($value); + } + else { + // convert ISO 8601 to timestamp + $result = strtotime($value); + if(!$result) { + $result = null; + } + } } return $result; } diff --git a/lib/equal/data/adapt/adapters/json/DataAdapterJsonTime.class.php b/lib/equal/data/adapt/adapters/json/DataAdapterJsonTime.class.php index be1c50b67..4a5af4398 100644 --- a/lib/equal/data/adapt/adapters/json/DataAdapterJsonTime.class.php +++ b/lib/equal/data/adapt/adapters/json/DataAdapterJsonTime.class.php @@ -26,18 +26,24 @@ public function getType() { public function adaptIn($value, $usage, $locale='en') { $result = null; if(!is_null($value)) { - $count = substr_count($value, ':'); - list($hour, $minute, $second) = [0,0,0]; - if($count == 2) { - list($hour, $minute, $second) = sscanf($value, "%d:%d:%d"); + if(is_numeric($value)) { + // value is a timestamp, keep it + $result = intval($value); } - else if($count == 1) { - list($hour, $minute) = sscanf($value, "%d:%d"); + else { + $count = substr_count($value, ':'); + list($hour, $minute, $second) = [0,0,0]; + if($count == 2) { + list($hour, $minute, $second) = sscanf($value, "%d:%d:%d"); + } + else if($count == 1) { + list($hour, $minute) = sscanf($value, "%d:%d"); + } + else if($count == 0) { + $hour = $value; + } + $result = ($hour * 3600) + ($minute * 60) + $second; } - else if($count == 0) { - $hour = $value; - } - $result = ($hour * 3600) + ($minute * 60) + $second; } return $result; } diff --git a/lib/equal/data/adapt/adapters/sql/DataAdapterSqlDateTime.class.php b/lib/equal/data/adapt/adapters/sql/DataAdapterSqlDateTime.class.php index c74f322c9..576b38000 100644 --- a/lib/equal/data/adapt/adapters/sql/DataAdapterSqlDateTime.class.php +++ b/lib/equal/data/adapt/adapters/sql/DataAdapterSqlDateTime.class.php @@ -30,7 +30,7 @@ public function adaptIn($value, $usage, $locale='en') { list($year, $month, $day, $hour, $minute, $second) = sscanf($value, "%d-%d-%d %d:%d:%d"); $result = mktime($hour, $minute, $second, $month, $day, $year); } - return $result; + return $result ?? null; } /** diff --git a/lib/equal/db/DBManipulator.class.php b/lib/equal/db/DBManipulator.class.php index 95870ff18..eebe6068d 100644 --- a/lib/equal/db/DBManipulator.class.php +++ b/lib/equal/db/DBManipulator.class.php @@ -180,9 +180,6 @@ public function canConnect() { return false; } - public function createDatabase($db_name) { - } - /** * Sends a SQL query. * @@ -251,10 +248,10 @@ public function setRecords($table, $ids, $fields, $conditions=null, $id_field='i /** * 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 + * @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) {} @@ -266,6 +263,118 @@ public function deleteRecords($table, $ids, $conditions=null, $id_field='id') {} * * @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') { - } + public function incRecords($table, $ids, $field, $increment, $id_field='id') {} + + + /* + SQL request generation helpers + */ + + + /** + * Creates a new database. + * + * Generates a SQL query and create a new database according to given $db_name. + * + * @param string $db_name The name of the database to create. + * @return string SQL query to create a database. + */ + public function createDatabase($db_name) {} + + /** + * Generates a SQL query to retrieve a list of all tables from the current database. + * + * @return string SQL query to retrieve all tables. + */ + public function getTables() {} + + /** + * Generates a SQL query to get the schema of the specified table. + * + * @param string $table_name The name of the table whose schema is to be retrieved. + * @return string SQL query to get the table schema. + */ + public function getTableSchema($table_name) {} + + /** + * Generates a SQL query to get the columns of the specified table. + * + * @param string $table_name The name of the table whose columns are to be retrieved. + * @return string SQL query to get the table columns. + */ + public function getTableColumns($table_name) {} + + /** + * Generates a SQL query to get the constraints of the specified table. + * + * @param string $table_name The name of the table whose constraints are to be retrieved. + * @return string SQL query to get the table constraints. + */ + public function getTableUniqueConstraints($table_name) {} + + /** + * Generates the SQL query to create a specific table. + * The query holds a condition to run only if the table does not exist yet. + * + * @param string $table_name The name of the table for which the creation SQL is generated. + * @return string SQL query to create the specified table. + */ + public function getQueryCreateTable($table_name) {} + + /** + * Generates one or more SQL queries related to a column creation, according to a given column definition. + * + * $def structure: + * [ + * 'type' => int(11), + * 'null' => false, + * 'default' => 0, + * 'auto_increment' => false, + * 'primary' => false, + * 'index' => false + * ] + * @param string $table_name The name of the table to modify. + * @param string $column_name The name of the column to add. + * @param array $def Array describing the column properties such as type, nullability, default value, etc. + * @return string SQL query to add a column. + */ + public function getQueryAddColumn($table_name, $column_name, $def) {} + + /** + * Generates a SQL query to add an index to a table. + * + * @param string $table_name The name of the table. + * @param string $column The name of the column to index. + * @return string SQL query to add an index. + */ + public function getQueryAddIndex($table_name, $column) {} + + /** + * Generates a SQL query to add a unique constraint to one or more columns in a table. + * + * @param string $table_name The name of the table. + * @param array|string $columns The name(s) of the column(s) to include in the unique constraint. + * @return string SQL query to add a unique constraint. + */ + public function getQueryAddUniqueConstraint($table_name, $columns) {} + + /** + * Generates a SQL query to add records to a table. + * + * @param string $table The name of the table where records will be added. + * @param array $fields Array of field names corresponding to the columns in the table. + * @param array $values Array of values to be inserted; each sub-array corresponds to a row. + * @return string SQL query to add records. + */ + public function getQueryAddRecords($table, $fields, $values) {} + + /** + * Generates a SQL query to set columns according to an associative array, for all records of a table. + * + * @param string $table Name of the table where records will be added. + * @param array $fields Associative array mapping field names (columns) to values those must be updated to. + * @param array $values Array of values to be inserted; each sub-array corresponds to a row. + * @return string SQL query to add records. + */ + public function getQuerySetRecords($table, $fields) {} } diff --git a/lib/equal/db/DBManipulatorMySQL.class.php b/lib/equal/db/DBManipulatorMySQL.class.php index b3e68d177..00a350e91 100644 --- a/lib/equal/db/DBManipulatorMySQL.class.php +++ b/lib/equal/db/DBManipulatorMySQL.class.php @@ -143,7 +143,7 @@ public function getTableColumns($table_name) { return $columns; } - public function getTableConstraints($table_name) { + public function getTableUniqueConstraints($table_name) { $query = "SELECT * FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS WHERE TABLE_SCHEMA = '{$this->db_name}' AND TABLE_NAME = '$table_name' AND CONSTRAINT_TYPE = 'UNIQUE';"; $res = $this->sendQuery($query); $constraints = []; @@ -166,19 +166,6 @@ public function getQueryCreateTable($table_name) { return $query.";"; } - /** - * Generates one or more SQL queries related to a column creation, according to given column definition. - * - * $def structure: - * [ - * 'type' => int(11), - * 'null' => false, - * 'default' => 0, - * 'auto_increment' => false, - * 'primary' => false, - * 'index' => false - * ] - */ public function getQueryAddColumn($table_name, $column_name, $def) { $sql = "ALTER TABLE `{$table_name}` ADD COLUMN `{$column_name}` {$def['type']}"; if(isset($def['null']) && !$def['null']) { @@ -202,7 +189,11 @@ public function getQueryAddColumn($table_name, $column_name, $def) { return $sql; } - public function getQueryAddConstraint($table_name, $columns) { + public function getQueryAddIndex($table_name, $column) { + return "ALTER TABLE `{$table_name}` ADD INDEX(`".$column."`);"; + } + + public function getQueryAddUniqueConstraint($table_name, $columns) { return "ALTER TABLE `{$table_name}` ADD CONSTRAINT ".implode('_', $columns)." UNIQUE (`".implode('`,`', $columns)."`);"; } @@ -232,6 +223,26 @@ public function getQueryAddRecords($table, $fields, $values) { return $sql; } + public function getQuerySetRecords($table, $fields) { + $sql = ''; + // test values and types + if(empty($table)) { + throw new \Exception(__METHOD__." : unable to build sql query, parameter 'table' empty.", QN_ERROR_SQL); + } + if(empty($fields)) { + throw new \Exception(__METHOD__." : unable to build sql query, parameter 'fields' empty.", QN_ERROR_SQL); + } + // UPDATE clause + $sql = 'UPDATE `'.$table.'`'; + // SET clause + $sql .= ' SET '; + foreach($fields as $key => $value) { + $sql .= "`$key`={$this->escapeString($value)}, "; + } + $sql = rtrim($sql, ', '); + return $sql.';'; + } + /** * Sends a SQL query. * @@ -480,34 +491,12 @@ public function getRecords($tables, $fields=NULL, $ids=NULL, $conditions=NULL, $ } public function setRecords($table, $ids, $fields, $conditions=null, $id_field='id'){ - // test values and types - if(empty($table)) { - throw new \Exception(__METHOD__." : unable to build sql query, parameter 'table' empty.", QN_ERROR_SQL); - } - if(empty($fields)) { - throw new \Exception(__METHOD__." : unable to build sql query, parameter 'fields' empty.", QN_ERROR_SQL); - } - - // UPDATE clause - $sql = 'UPDATE `'.$table.'`'; - - // SET clause - $sql .= ' SET '; - foreach ($fields as $key => $value) { - $sql .= "`$key`={$this->escapeString($value)}, "; - } - $sql = rtrim($sql, ', '); - - // WHERE clause + $sql = rtrim($this->getQuerySetRecords($table, $fields), ';'); $sql .= $this->getConditionClause($id_field, $ids, $conditions); - return $this->sendQuery($sql); } 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); - } $sql = $this->getQueryAddRecords($table, $fields, $values); return $this->sendQuery($sql); } diff --git a/lib/equal/db/DBManipulatorSQLite.class.php b/lib/equal/db/DBManipulatorSQLite.class.php index 0574cbf98..b8a80b916 100644 --- a/lib/equal/db/DBManipulatorSQLite.class.php +++ b/lib/equal/db/DBManipulatorSQLite.class.php @@ -147,7 +147,7 @@ public function getTableColumns($table_name) { return $columns; } - public function getTableConstraints($table_name) { + public function getTableUniqueConstraints($table_name) { $query = "PRAGMA table_info($table_name);"; $res = $this->sendQuery($query); $constraints = []; @@ -212,7 +212,11 @@ public function getQueryAddColumn($table_name, $column_name, $def) { return $sql; } - public function getQueryAddConstraint($table_name, $columns) { + public function getQueryAddIndex($table_name, $column) { + return "CREATE INDEX idx_{$column} ON `{$table_name}` (`{$column}`);"; + } + + public function getQueryAddUniqueConstraint($table_name, $columns) { return "CREATE UNIQUE INDEX ".implode('_', $columns)." ON {$table_name}(".implode(',', $columns).");"; } @@ -242,6 +246,28 @@ public function getQueryAddRecords($table, $fields, $values) { return $sql; } + public function getQuerySetRecords($table, $fields) { + $sql = ''; + // test values and types + if(empty($table)) { + throw new \Exception(__METHOD__." : unable to build sql query, parameter 'table' empty.", QN_ERROR_SQL); + } + if(empty($fields)) { + throw new \Exception(__METHOD__." : unable to build sql query, parameter 'fields' empty.", QN_ERROR_SQL); + } + + // UPDATE clause + $sql = 'UPDATE `'.$table.'`'; + + // SET clause + $sql .= ' SET '; + foreach ($fields as $key => $value) { + $sql .= "`$key`={$this->escapeString($value)}, "; + } + $sql = rtrim($sql, ', '); + return $sql.';'; + } + /** * Sends a SQL query. * @@ -494,34 +520,12 @@ public function getRecords($tables, $fields=NULL, $ids=NULL, $conditions=NULL, $ } public function setRecords($table, $ids, $fields, $conditions=null, $id_field='id'){ - // test values and types - if(empty($table)) { - throw new \Exception(__METHOD__." : unable to build sql query, parameter 'table' empty.", QN_ERROR_SQL); - } - if(empty($fields)) { - throw new \Exception(__METHOD__." : unable to build sql query, parameter 'fields' empty.", QN_ERROR_SQL); - } - - // UPDATE clause - $sql = 'UPDATE `'.$table.'`'; - - // SET clause - $sql .= ' SET '; - foreach ($fields as $key => $value) { - $sql .= "`$key`={$this->escapeString($value)}, "; - } - $sql = rtrim($sql, ', '); - - // WHERE clause + $sql = rtrim($this->getQuerySetRecords($table, $fields), ';'); $sql .= $this->getConditionClause($id_field, $ids, $conditions); - return $this->sendQuery($sql); } 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); - } $sql = $this->getQueryAddRecords($table, $fields, $values); return $this->sendQuery($sql); } diff --git a/lib/equal/db/DBManipulatorSqlSrv.class.php b/lib/equal/db/DBManipulatorSqlSrv.class.php index c859ec3cf..1758a872a 100644 --- a/lib/equal/db/DBManipulatorSqlSrv.class.php +++ b/lib/equal/db/DBManipulatorSqlSrv.class.php @@ -163,7 +163,7 @@ public function getTableColumns($table_name) { return $columns; } - public function getTableConstraints($table_name) { + public function getTableUniqueConstraints($table_name) { $query = "SELECT * FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS WHERE TABLE_CATALOG = '{$this->db_name}' AND TABLE_NAME = '$table_name' AND CONSTRAINT_TYPE = 'UNIQUE';"; $res = $this->sendQuery($query); $constraints = []; @@ -225,12 +225,15 @@ public function getQueryAddColumn($table_name, $column_name, $def) { return $sql; } - public function getQueryAddConstraint($table_name, $columns) { - return "ALTER TABLE [$table_name] ADD CONSTRAINT ".implode('_', array_merge(['AK'], $columns))." UNIQUE (".implode(',', $columns).");"; + public function getQueryAddIndex($table_name, $column) { + return "CREATE INDEX idx_{$column} ON [$table_name] ($column);"; } + public function getQueryAddUniqueConstraint($table_name, $columns) { + return "ALTER TABLE [$table_name] ADD CONSTRAINT ".implode('_', array_merge(['AK'], $columns))." UNIQUE (".implode(',', $columns).");"; + } - public function getQueryAddRecords($table, $fields, $values) { + public function getQueryAddRecords($table_name, $fields, $values) { $sql = ''; if (!is_array($fields) || !is_array($values)) { throw new \Exception(__METHOD__.' : at least one parameter is missing', QN_ERROR_SQL); @@ -245,14 +248,36 @@ public function getQueryAddRecords($table, $fields, $values) { } if(count($fields) && count($vals)) { // #todo ignore duplicate entries, if any - $sql = "INSERT INTO [$table] (".implode(',', $fields).") OUTPUT INSERTED.id VALUES ".implode(',', $vals).";"; + $sql = "INSERT INTO [$table_name] (".implode(',', $fields).") OUTPUT INSERTED.id VALUES ".implode(',', $vals).";"; if(in_array('id', $fields)) { - $sql = "SET IDENTITY_INSERT $table ON;".$sql."SET IDENTITY_INSERT $table OFF;"; + $sql = "SET IDENTITY_INSERT $table_name ON;".$sql."SET IDENTITY_INSERT $table_name OFF;"; } } return $sql; } + public function getQuerySetRecords($table, $fields) { + $sql = ''; + // test values and types + if(empty($table)) { + throw new \Exception(__METHOD__." : unable to build sql query, parameter 'table' empty.", QN_ERROR_SQL); + } + if(empty($fields)) { + throw new \Exception(__METHOD__." : unable to build sql query, parameter 'fields' empty.", QN_ERROR_SQL); + } + + // UPDATE clause + $sql = "UPDATE [{$table}]"; + + // SET clause + $sql .= ' SET '; + foreach ($fields as $key => $value) { + $sql .= "[$key]={$this->escapeString($value)}, "; + } + $sql = rtrim($sql, ', '); + return $sql.';'; + } + /** * Sends a SQL query. * @@ -487,34 +512,12 @@ public function getRecords($tables, $fields=NULL, $ids=NULL, $conditions=NULL, $ } public function setRecords($table, $ids, $fields, $conditions=null, $id_field='id'){ - // test values and types - if(empty($table)) { - throw new \Exception(__METHOD__." : unable to build sql query, parameter 'table' empty.", QN_ERROR_SQL); - } - if(empty($fields)) { - throw new \Exception(__METHOD__." : unable to build sql query, parameter 'fields' empty.", QN_ERROR_SQL); - } - - // UPDATE clause - $sql = "UPDATE [{$table}]"; - - // SET clause - $sql .= ' SET '; - foreach ($fields as $key => $value) { - $sql .= "[$key]={$this->escapeString($value)}, "; - } - $sql = rtrim($sql, ', '); - - // WHERE clause + $sql = rtrim($this->getQuerySetRecords($table, $fields), ';'); $sql .= $this->getConditionClause($id_field, $ids, $conditions); - return $this->sendQuery($sql, 'update'); } 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); - } $sql = $this->getQueryAddRecords($table, $fields, $values); return $this->sendQuery($sql, 'insert'); } diff --git a/lib/equal/http/HttpMessage.class.php b/lib/equal/http/HttpMessage.class.php index afb91c732..0420d2855 100644 --- a/lib/equal/http/HttpMessage.class.php +++ b/lib/equal/http/HttpMessage.class.php @@ -336,13 +336,13 @@ public function setBody($body, $raw=false) { if (empty($block)) continue; // parse uploaded files if (strpos($block, 'application/octet-stream') !== FALSE) { - // match "name", then everything after "stream" (optional) except for prepending newlines - preg_match("/name=\"([^\"]*)\".*stream[\n|\r]+([^\n\r].*)?$/s", $block, $matches); + // match "name", then everything after "stream" (optional) except for prepending newlines + preg_match("/name=\"([^\"]*)\".*stream[\n|\r]+([^\n\r].*)?$/s", $block, $matches); } // parse all other fields else { - // match "name" and optional value in between newline sequences - preg_match('/name=\"([^\"]*)\"[\n|\r]+([^\n\r].*)?\r$/s', $block, $matches); + // match "name" and optional value in between newline sequences + preg_match('/name=\"([^\"]*)\"[\n|\r]+([^\n\r].*)?\r$/s', $block, $matches); } // assign param/value pair to related body index $request[$matches[1]] = $matches[2]; diff --git a/lib/equal/orm/Domain.class.php b/lib/equal/orm/Domain.class.php index ef038dda1..b24b3126b 100644 --- a/lib/equal/orm/Domain.class.php +++ b/lib/equal/orm/Domain.class.php @@ -14,7 +14,7 @@ * '{operand}', '{operator}', '{value}' // condition * ], * [ - * '{operand}', '{operator}', '{value}' // another contition (AND) + * '{operand}', '{operator}', '{value}' // another condition (AND) * ] * ], * [ // another clause (OR) @@ -22,7 +22,7 @@ * '{operand}', '{operator}', '{value}' // condition * ], * [ - * '{operand}', '{operator}', '{value}' // another contition (AND) + * '{operand}', '{operator}', '{value}' // another condition (AND) * ] * ] * ]; diff --git a/lib/equal/orm/Field.class.php b/lib/equal/orm/Field.class.php index c34fd7fb1..30d2db43f 100644 --- a/lib/equal/orm/Field.class.php +++ b/lib/equal/orm/Field.class.php @@ -73,6 +73,8 @@ protected function getUsageString(): string { 'time' => 'time/plain', 'binary' => 'binary/plain:64000000', 'many2one' => 'number/integer:9', + 'one2many' => 'array', + 'many2many' => 'array', 'array' => 'array' ]; $type = $this->type; @@ -106,6 +108,9 @@ public function getConstraints(): array { // generate constraint based on type $result_type = $this->descriptor['result_type']; + + /* + // #memo - strict constraint is not relevant since lose conversion is possible for some types (e.g. "30" is an accepted integer) $constraints['invalid_type'] = [ 'message' => "Value is not of type {$result_type}.", 'function' => function($value) use($result_type) { @@ -128,6 +133,7 @@ public function getConstraints(): array { return (gettype($value) == $mapped_type); } ]; + */ // add constraint based on selection, if present if(isset($this->descriptor['selection']) && count($this->descriptor['selection'])) { diff --git a/lib/equal/orm/Model.class.php b/lib/equal/orm/Model.class.php index b745bcd45..898923276 100644 --- a/lib/equal/orm/Model.class.php +++ b/lib/equal/orm/Model.class.php @@ -6,6 +6,7 @@ */ namespace equal\orm; +use equal\services\Container; /** * Root Model for all Object definitions. @@ -103,6 +104,8 @@ public final function __construct($values=[]) { } private function setDefaults($values=[]) { + $container = Container::getInstance(); + $orm = $container->get('orm'); $defaults = $this->getDefaults(); // reset fields values $this->values = []; @@ -114,8 +117,25 @@ private function setDefaults($values=[]) { $this->values[$field] = $values[$field]; } elseif(isset($defaults[$field])) { - // #memo - default value should be either a simple type, a PHP expression, or a PHP function (executed at definition parsing) - $this->values[$field] = $defaults[$field]; + if(is_callable($defaults[$field])) { + // either a php function (or a function from the global scope) or a closure object + if(is_object($defaults[$field])) { + // default is a closure + $this->values[$field] = $defaults[$field](); + } + else { + // do not call since there is an ambiguity (e.g. 'time') + $this->values[$field] = $defaults[$field]; + } + } + elseif(is_string($defaults[$field]) && method_exists($this->getType(), $defaults[$field])) { + // default is a method of the class (or parents') + $this->values[$field] = $orm->callonce($this->getType(), $defaults[$field]); + } + else { + // default is a scalar value + $this->values[$field] = $defaults[$field]; + } } } } @@ -223,7 +243,7 @@ public final static function getSpecialColumns() { ], 'created' => [ 'type' => 'datetime', - 'default' => time(), + 'default' => function() { return time(); }, 'readonly' => true ], 'modifier' => [ @@ -233,7 +253,7 @@ public final static function getSpecialColumns() { ], 'modified' => [ 'type' => 'datetime', - 'default' => time(), + 'default' => function() { return time(); }, 'readonly' => true ], // #memo - when set to true, modifier points to the user who deleted the object and modified is the time of the deletion @@ -315,6 +335,7 @@ public final function getFields() { /** * Returns a Field object that corresponds to the descriptor of the given field (from schema). + * If a field is an alias, it is the final descriptor of the alias chain that is returned. * * @return Field Associative array mapping fields names with their related Field instances. */ @@ -559,4 +580,4 @@ public static function __callStatic($name, $arguments) { return null; } -} \ No newline at end of file +} diff --git a/lib/equal/orm/ObjectManager.class.php b/lib/equal/orm/ObjectManager.class.php index 12527a60a..bb12bf625 100644 --- a/lib/equal/orm/ObjectManager.class.php +++ b/lib/equal/orm/ObjectManager.class.php @@ -260,7 +260,7 @@ public function getPackages() { * @param string $class The full name of the class with its namespace. * @param array $fields Associative array mapping fields with default values (provided by create() method). * @throws Exception - * @return Object Returns a partial instance of the targeted class. + * @return Model Returns a partial instance of the targeted class. */ private function getStaticInstance($class, $fields=[]) { if(count($fields) || !isset($this->models[$class])) { @@ -1041,7 +1041,7 @@ private function store($class, $ids, $fields, $lang) { * @param array $signature (deprecated) List of parameters to relay to target method (required if differing from default). * @return mixed Returns the result of the called method (defaults to empty array), or error code (negative int) if something went wrong. */ - public function callonce($class, $method, $ids, $values=[], $lang=null, $signature=['ids', 'values', '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) @@ -1092,12 +1092,28 @@ public function callonce($class, $method, $ids, $values=[], $lang=null, $signatu if(in_array($param_name, ['om', 'orm'])) { $args[] = $this; } + elseif(in_array($param_name, [ + 'report', + 'auth', + 'access', + 'context', + 'validate', + 'adapt', + 'route', + 'log', + 'cron', + 'dispatch', + 'db'])) { + $args[] = $this->container->get($param_name); + } + elseif(in_array($param_name, ['ids', 'oids'])) { $args[] = $unprocessed_ids; } elseif($param_name == 'values') { $args[] = $values; } + // #todo - deprecate : use $auth instead elseif($param_name == 'user_id') { $auth = $this->container->get('auth'); $user_id = $auth->userId(); @@ -1204,7 +1220,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. + * @return boolean|Model Returns the static instance of the model with default values. If no Model matches the class name returns false. */ public function getModel($class) { $model = false; @@ -1213,7 +1229,7 @@ public function getModel($class) { } catch(Exception $e) { trigger_error($e->getMessage(), QN_REPORT_ERROR); - // #memo - another autoload handler might be registered, so we relay without raising an exception + // #memo - another autoload handler might be registered, so no exception can be raised } return $model; } @@ -1302,7 +1318,7 @@ public function validate($class, $ids, $values, $check_unique=false, $check_requ continue; } if($value === null) { - // all fields can be reset to null + // all fields can be reset to null (unless required) continue; } foreach($constraints[$field] as $error_id => $constraint) { @@ -1636,11 +1652,15 @@ public function update($class, $ids=null, $fields=null, $lang=null, $create=fals $fields['modified'] = time(); - // 4) call 'onupdate' hook : notify objects that they're about to be updated with given values + // 4) call 'onbeforeupdate' hook : notify objects that they're about to be updated with given values if(!$create) { - // #todo - allow explicit notation `onbeforeupdate()` - $this->callonce($class, 'onupdate', $ids, $fields, $lang); + if(method_exists($class, 'onbeforeupdate')) { + $this->callonce($class, 'onbeforeupdate', $ids, $fields, $lang); + } + else { + $this->callonce($class, 'onupdate', $ids, $fields, $lang); + } } @@ -1719,7 +1739,7 @@ public function update($class, $ids=null, $fields=null, $lang=null, $create=fals ]; foreach($fields as $field => $value) { // remember fields whose modification triggers resetting computed fields - // #todo - deprecate dependencies : use dependents + // #todo - deprecate 'dependencies' : use dependents if(isset($schema[$field]['dependencies'])) { foreach((array) $schema[$field]['dependencies'] as $dependent) { $dependents['primary'][$dependent] = true; @@ -2523,7 +2543,7 @@ public function search($class, $domain=null, $sort=['id' => 'asc'], $start='0', if( $operator == '=') { $operator = 'is'; } - else if( $operator == '<>') { + elseif( $operator == '<>') { $operator = 'is not'; } } diff --git a/lib/equal/orm/usages/UsageArray.class.php b/lib/equal/orm/usages/UsageArray.class.php index a211e0b51..23f6200d7 100644 --- a/lib/equal/orm/usages/UsageArray.class.php +++ b/lib/equal/orm/usages/UsageArray.class.php @@ -15,6 +15,14 @@ class UsageArray extends Usage { */ public function getConstraints(): array { return [ + + 'not_array' => [ + 'message' => 'Value is not an array.', + 'function' => function($value) { + return is_array($value); + } + ] + ]; } diff --git a/lib/equal/orm/usages/UsageDate.class.php b/lib/equal/orm/usages/UsageDate.class.php index 7d2bfda90..80bb2763f 100644 --- a/lib/equal/orm/usages/UsageDate.class.php +++ b/lib/equal/orm/usages/UsageDate.class.php @@ -24,12 +24,22 @@ class UsageDate extends Usage { datetime (ISO 8601) */ public function getConstraints(): array { + $constraints = [ + 'invalid_type' => [ + 'message' => 'Value is incompatible with type datetime.', + 'function' => function($value) { + return (gettype($value) == 'integer'); + } + ] + ]; $subtype = $this->getSubtype(); $main_subtype = ( explode('.', $subtype) )[0]; + + ; switch($main_subtype) { case 'day': - return [ - 'invalid_amount' => [ + $constraints[] = [ + 'invalid_date' => [ 'message' => 'Malformed day value.', 'function' => function($value) { // 2 digits, from 1 to 31 @@ -37,9 +47,10 @@ public function getConstraints(): array { } ] ]; + break; case 'month': - return [ - 'invalid_amount' => [ + $constraints[] = [ + 'invalid_date' => [ 'message' => 'Malformed month value.', 'function' => function($value) { // 2 digits, from 1 to 12 @@ -47,9 +58,10 @@ public function getConstraints(): array { } ] ]; + break; case 'year': - return [ - 'invalid_amount' => [ + $constraints[] = [ + 'invalid_date' => [ 'message' => 'Malformed year value.', 'function' => function($value) { // 4 digits, from 0 to 9999 @@ -57,17 +69,18 @@ public function getConstraints(): array { } ] ]; + break; default: - return [ - 'invalid_amount' => [ - 'message' => 'Malformed amount or size overflow.', + $constraints[] = [ + 'invalid_date' => [ + 'message' => 'Malformed date or unknown format.', 'function' => function($value) { return ($value <= PHP_INT_MAX && $value >= 0); } ] ]; } - return []; + return $constraints; } } diff --git a/packages/core/actions/init/composer.php b/packages/core/actions/init/composer.php index 862742872..0fb9f347d 100644 --- a/packages/core/actions/init/composer.php +++ b/packages/core/actions/init/composer.php @@ -1,14 +1,14 @@ - Some Rights Reserved, Cedric Francoys, 2010-2021 + This file is part of the eQual framework + Some Rights Reserved, Cedric Francoys, 2010-2024 Licensed under GNU GPL 3 license */ use equal\http\HttpRequest; use equal\http\HttpResponse; -list($params, $providers) = announce([ +list($params, $providers) = eQual::announce([ 'description' => "Downloads composer and runs it for installing dependencies from composer.json.", 'help' => "This controller rely on the PHP binary. In order to make them work, sure the PHP binary is present in the PATH.", 'params' => [], @@ -21,45 +21,55 @@ list($context) = [$providers['context']]; // stop if composer.json is missing -if(!file_exists('composer.json')) { - throw new Exception('missing_composer_json', QN_ERROR_MISSING_PARAM); +if(!file_exists(EQ_BASEDIR.'/composer.json')) { + throw new Exception('missing_composer_json', EQ_ERROR_MISSING_PARAM); } -// retrieve the checksum on github -$request = new HttpRequest('GET https://composer.github.io/installer.sig'); -/** @var HttpResponse */ -$response = $request->send(); -$expected_checksum = $response->body(); +if(!file_exists(EQ_BASEDIR.'/composer.phar')) { + // retrieve the checksum on github + $request = new HttpRequest('GET https://composer.github.io/installer.sig'); + /** @var HttpResponse */ + $response = $request->send(); + $expected_checksum = $response->body(); -// download composer-setup script -copy('https://getcomposer.org/installer', 'composer-setup.php'); + // download composer-setup script + copy('https://getcomposer.org/installer', EQ_BASEDIR.'/composer-setup.php'); -// if something went wrong during download, stop -if(!file_exists('composer-setup.php')) { - throw new Exception('missing_file', QN_ERROR_UNKNOWN_OBJECT); -} + // if something went wrong during download, stop + if(!file_exists(EQ_BASEDIR.'/composer-setup.php')) { + throw new Exception('missing_file', EQ_ERROR_UNKNOWN_OBJECT); + } -// make sure checksum if consistent -if(hash_file('sha384', 'composer-setup.php') !== $expected_checksum) { - throw new Exception('invalid_checksum', QN_ERROR_UNKNOWN_OBJECT); -} + // make sure checksum if consistent + if(hash_file('sha384', EQ_BASEDIR.'/composer-setup.php') !== $expected_checksum) { + throw new Exception('invalid_checksum', EQ_ERROR_UNKNOWN_OBJECT); + } -// run setup and remove script afterward -if(exec('php composer-setup.php --quiet') === false) { - throw new Exception('command_failed', QN_ERROR_UNKNOWN); -} -unlink('composer-setup.php'); + // run setup and remove script afterward + if(exec('php composer-setup.php --quiet') === false) { + throw new Exception('command_failed', EQ_ERROR_UNKNOWN); + } + unlink(EQ_BASEDIR.'/composer-setup.php'); -// check the presence of the executable -if(!file_exists('composer.phar')) { - throw new Exception('install_failed', QN_ERROR_UNKNOWN); + // check the presence of the executable + if(!file_exists(EQ_BASEDIR.'/composer.phar')) { + throw new Exception('install_failed', EQ_ERROR_UNKNOWN); + } } -// run composer to install dependencies (quiet mode, no interactions) -if(exec('php composer.phar install -q -n') === false) { - throw new Exception('composer_failed', QN_ERROR_UNKNOWN); +if(!file_exists(EQ_BASEDIR.'/composer.lock')) { + // run composer to install dependencies (quiet mode, no interactions) + if(exec('php composer.phar install -q -n') === false) { + throw new Exception('composer_failed', EQ_ERROR_UNKNOWN); + } +} +else { + // run composer update + if(exec('php composer.phar update -q -n') === false) { + throw new Exception('composer_failed', EQ_ERROR_UNKNOWN); + } } $context->httpResponse() ->status(204) - ->send(); \ No newline at end of file + ->send(); diff --git a/packages/core/actions/init/package.php b/packages/core/actions/init/package.php index 7b35b2d5c..546b90219 100644 --- a/packages/core/actions/init/package.php +++ b/packages/core/actions/init/package.php @@ -26,7 +26,7 @@ 'default' => true ], 'import' => [ - 'description' => 'Request for importing initial data.', + 'description' => 'Request importing initial data.', 'type' => 'boolean', 'default' => false ], @@ -34,6 +34,21 @@ 'description' => 'Import initial data for dependencies as well.', 'type' => 'boolean', 'default' => true + ], + 'demo' => [ + 'description' => 'Request importing demo data.', + 'type' => 'boolean', + 'default' => false + ], + 'composer' => [ + 'description' => 'Flag for requesting initialization of composer dependencies.', + 'type' => 'boolean', + 'default' => true + ], + 'root' => [ + 'description' => 'Mark the script as top-level or as a sub-call (for recursion).', + 'type' => 'boolean', + 'default' => true ] ], 'constants' => ['DB_HOST', 'DB_PORT', 'DB_NAME', 'DB_USER', 'DB_PASSWORD', 'DB_DBMS'], @@ -80,7 +95,7 @@ } } -// check if there are dependencies +// check if there are dependencies (must be initialized beforehand) if($params['cascade'] && isset($package_manifest['depends_on']) && is_array($package_manifest['depends_on'])) { // initiate dependency packages that are not yet processed, if requested foreach($package_manifest['depends_on'] as $dependency) { @@ -90,7 +105,8 @@ eQual::run('do', 'init_package', [ 'package' => $dependency, 'cascade' => $params['cascade'], - 'import' => $params['import'] && $params['import_cascade'] + 'import' => $params['import'] && $params['import_cascade'], + 'root' => false ], true); } @@ -106,7 +122,6 @@ // 1) Init DB with SQL schema /* start-tables_init */ - // retrieve schema for given package $data = eQual::run('get', 'utils_sql-schema', ['package' => $params['package'], 'full' => false]); @@ -117,11 +132,8 @@ foreach($queries as $query) { $db->sendQuery($query); } - /* end-tables_init */ -// #todo : make distinction between mandatory initial data and demo data - // 2) Populate tables with predefined data $data_folder = "packages/{$params['package']}/init/data"; if($params['import'] && file_exists($data_folder) && is_dir($data_folder)) { @@ -173,6 +185,57 @@ } } +// 2 bis) Populate tables with demo data, if requested +$demo_folder = "packages/{$params['package']}/init/demo"; +if($params['demo'] && file_exists($demo_folder) && is_dir($demo_folder)) { + // handle JSON files + foreach (glob($demo_folder."/*.json") as $json_file) { + $data = file_get_contents($json_file); + $classes = json_decode($data, true); + foreach($classes as $class) { + $entity = $class['name']; + $lang = $class['lang']; + $model = $orm->getModel($entity); + $schema = $model->getSchema(); + + $objects_ids = []; + + foreach($class['data'] as $odata) { + foreach($odata as $field => $value) { + $f = new Field($schema[$field]); + $odata[$field] = $adapter->adaptIn($value, $f->getUsage()); + } + + if(isset($odata['id'])) { + $res = $orm->search($entity, ['id', '=', $odata['id']]); + if($res > 0 && count($res)) { + // object already exist, but either values or language might differ + } + else { + $orm->create($entity, ['id' => $odata['id']], $lang, false); + } + $id = $odata['id']; + unset($odata['id']); + } + else { + $id = $orm->create($entity, [], $lang); + } + $orm->update($entity, $id, $odata, $lang); + $objects_ids[] = $id; + } + + // force a first generation of computed fields, if any + $computed_fields = []; + foreach($schema as $field => $def) { + if($def['type'] == 'computed') { + $computed_fields[] = $field; + } + } + $orm->read($entity, $objects_ids, $computed_fields, $lang); + } + } +} + // 3) If a `bin` folder exists, copy its content to /bin// $bin_folder = "packages/{$params['package']}/init/bin"; if($params['import'] && file_exists($bin_folder) && is_dir($bin_folder)) { @@ -186,6 +249,11 @@ exec("cp -r $route_folder/* config/routing"); } +$assets_folder = "packages/{$params['package']}/init/assets"; +if(file_exists($assets_folder) && is_dir($assets_folder)) { + exec("cp -r $assets_folder/* public/assets/"); +} + // 5) Export the compiled apps to related public folders if(isset($package_manifest['apps']) && is_array($package_manifest['apps'])) { @@ -253,6 +321,28 @@ } } +// 6) Inject composer dependencies if any +if(isset($package_manifest['requires']) && is_array($package_manifest['requires'])) { + $map_composer = [ + 'require' => [], + 'require-dev' => [] + ]; + + if(file_exists(EQ_BASEDIR.'/composer.json')) { + $json = file_get_contents(EQ_BASEDIR.'/composer.json'); + $data = json_decode($json, true); + if($data) { + $map_composer = $data; + } + } + + foreach($package_manifest['requires'] as $dependency => $version) { + $map_composer['require'][$dependency] = $version; + } + + file_put_contents(EQ_BASEDIR.'/composer.json', json_encode($map_composer, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); +} + // mark the package as initialized (installed) /** @@ -269,6 +359,11 @@ $packages[$params['package']] = date('c'); file_put_contents("log/packages.json", json_encode($packages, JSON_PRETTY_PRINT)); +// if script is running at top-level, run composer to install vendor dependencies +if($params['root'] && $params['composer']) { + eQual::run('do', 'init_composer'); +} + $context->httpResponse() ->status(201) ->send(); diff --git a/packages/core/actions/model/update.php b/packages/core/actions/model/update.php index 66164563a..148158945 100644 --- a/packages/core/actions/model/update.php +++ b/packages/core/actions/model/update.php @@ -1,13 +1,11 @@ - Some Rights Reserved, Cedric Francoys, 2010-2021 + This file is part of the eQual framework + Some Rights Reserved, Cedric Francoys, 2010-2024 Licensed under GNU LGPL 3 license */ -use equal\orm\Field; - -list($params, $providers) = announce([ +list($params, $providers) = eQual::announce([ 'description' => "Update (fully or partially) the given object.", 'params' => [ 'entity' => [ @@ -88,19 +86,21 @@ ); foreach($fields as $field => $value) { - if(!isset($schema[$field])) { + $f = $model->getField($field); + $descriptor = $f->getDescriptor(); + $type = $descriptor['result_type']; + // drop empty fields : non-string scalar fields with empty string as value are ignored (unless set to null) + if(!is_array($value) && !strlen(strval($value)) && !in_array($type, ['boolean', 'string', 'text']) && !is_null($value)) { + unset($fields[$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) ) { - unset($fields[$field]); + // empty strings are considered equivalent to null + if(in_array($type, ['string', 'text']) && !strlen(strval($value))) { + $fields[$field] = null; continue; } try { // adapt received values based on their type (as defined in schema) - /** @var equal\orm\Field */ - $f = $model->getField($field); $fields[$field] = $adapter->adaptIn($value, $f->getUsage()); } catch(Exception $e) { @@ -116,7 +116,7 @@ if(count($fields)) { - // we're updating a single object: enforce Optimistic Concurrency Control (https://en.wikipedia.org/wiki/Optimistic_concurrency_control) + // when updating a single object, enforce Optimistic Concurrency Control (https://en.wikipedia.org/wiki/Optimistic_concurrency_control) if(count($params['ids']) == 1) { // handle draft edition if(isset($fields['state']) && $fields['state'] == 'draft') { @@ -133,7 +133,7 @@ } } // handle instances edition - else if(isset($fields['modified']) ) { + elseif(isset($fields['modified']) ) { $object = $params['entity']::ids($params['ids'])->read(['modified'])->first(true); // a changed occurred in the meantime if($object['modified'] != $fields['modified'] && !$params['force']) { @@ -151,6 +151,6 @@ } $context->httpResponse() - ->status(200) - ->body($result) - ->send(); \ No newline at end of file + ->status(200) + ->body($result) + ->send(); diff --git a/packages/core/actions/test/package-consistency.php b/packages/core/actions/test/package-consistency.php index 4a6a9e8e9..4f41a6cb1 100644 --- a/packages/core/actions/test/package-consistency.php +++ b/packages/core/actions/test/package-consistency.php @@ -1,7 +1,7 @@ - Some Rights Reserved, Cedric Francoys, 2010-2021 + This file is part of the eQual framework + Some Rights Reserved, Cedric Francoys, 2010-2024 Licensed under GNU GPL 3 license */ use equal\db\DBConnector; @@ -42,16 +42,19 @@ /* -#TODO : check config and json files syntax. + #todo : -* translation files constraints: - * model helpers : max 45 chars - * error messages length : max 45 chars + * check config and json files syntax. -* for each view, check that each field is present 0 or 1 time -* for each view, check that the id match an entry in the translation file -* for each class, check that all fields are each field is present at least in one view + * translation files constraints: + * model helpers : max 45 chars + * error messages length : max 45 chars + * for each view, check that each field is present 0 or 1 time + * for each view, check that the id match an entry in the translation file + * for each class, check that all fields are each field is present at least in one view + + * check for potential versions conflicts across packages in manifest `requires` (composer dependencies) */ // result of the tests : array containing errors (if no errors are found, array is empty) diff --git a/packages/core/apps/app/.gitignore b/packages/core/apps/app/.gitignore deleted file mode 100644 index eef52883d..000000000 --- a/packages/core/apps/app/.gitignore +++ /dev/null @@ -1 +0,0 @@ -source/ diff --git a/packages/core/apps/app/source b/packages/core/apps/app/source new file mode 160000 index 000000000..e28246dba --- /dev/null +++ b/packages/core/apps/app/source @@ -0,0 +1 @@ +Subproject commit e28246dba84e3531b3333a518b62b76821c3badf diff --git a/packages/core/apps/app/version b/packages/core/apps/app/version index a1f6fb45c..1db4b60ef 100644 --- a/packages/core/apps/app/version +++ b/packages/core/apps/app/version @@ -1 +1 @@ -79a94a04146558f800b2c45bf53edd37 +c2148916cb243c37ed4bf9f9750a90bd diff --git a/packages/core/apps/app/web.app b/packages/core/apps/app/web.app index 695033b61..08a15d4e3 100644 Binary files a/packages/core/apps/app/web.app and b/packages/core/apps/app/web.app differ diff --git a/packages/core/apps/apps/.gitignore b/packages/core/apps/apps/.gitignore deleted file mode 100644 index eef52883d..000000000 --- a/packages/core/apps/apps/.gitignore +++ /dev/null @@ -1 +0,0 @@ -source/ diff --git a/packages/core/apps/apps/source b/packages/core/apps/apps/source new file mode 160000 index 000000000..a5740f27a --- /dev/null +++ b/packages/core/apps/apps/source @@ -0,0 +1 @@ +Subproject commit a5740f27a425be67379e945a0dac18848a1648ee diff --git a/packages/core/apps/apps/version b/packages/core/apps/apps/version index 24dbaebb6..01e24de22 100644 --- a/packages/core/apps/apps/version +++ b/packages/core/apps/apps/version @@ -1 +1 @@ -487c4308680841d8d10fafe86a09f821 +f33e8b95796a225a2dd82c98fc728d36 diff --git a/packages/core/apps/apps/web.app b/packages/core/apps/apps/web.app index 79ff70d3e..4fe532be3 100644 Binary files a/packages/core/apps/apps/web.app and b/packages/core/apps/apps/web.app differ diff --git a/packages/core/apps/auth/.gitignore b/packages/core/apps/auth/.gitignore deleted file mode 100644 index eef52883d..000000000 --- a/packages/core/apps/auth/.gitignore +++ /dev/null @@ -1 +0,0 @@ -source/ diff --git a/packages/core/apps/auth/source b/packages/core/apps/auth/source new file mode 160000 index 000000000..d4698498a --- /dev/null +++ b/packages/core/apps/auth/source @@ -0,0 +1 @@ +Subproject commit d4698498a7cf776a41387e1f24d2f710ad464ec6 diff --git a/packages/core/apps/settings/.gitignore b/packages/core/apps/settings/.gitignore deleted file mode 100644 index eef52883d..000000000 --- a/packages/core/apps/settings/.gitignore +++ /dev/null @@ -1 +0,0 @@ -source/ diff --git a/packages/core/apps/settings/source b/packages/core/apps/settings/source new file mode 160000 index 000000000..97359b659 --- /dev/null +++ b/packages/core/apps/settings/source @@ -0,0 +1 @@ +Subproject commit 97359b659268799c89cf5f4eccc74f683c414545 diff --git a/packages/core/apps/settings/version b/packages/core/apps/settings/version index e2f75f94e..cc1412424 100644 --- a/packages/core/apps/settings/version +++ b/packages/core/apps/settings/version @@ -1 +1 @@ -d8cf9b211954aa8aaef6140701deaa7a +7b9dcd4be1e927e31ff78a3d5e1ca7d5 diff --git a/packages/core/apps/settings/web.app b/packages/core/apps/settings/web.app index 5dc14a818..3d8d1450c 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 deleted file mode 100644 index f5461d21f..000000000 --- a/packages/core/apps/welcome/index.html +++ /dev/null @@ -1,254 +0,0 @@ - - - - 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 deleted file mode 100644 index 7ddb96e7a..000000000 --- a/packages/core/apps/welcome/manifest.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "name": "Welcome", - "description": "Welcome screen displayed as default App.", - "version": "1.0", - "authors": ["Cedric Francoys"], - "license": "LGPL-3", - "repository": "https://github.com/equalframework/apps-core-welcome.git", - "url": "/welcome", - "icon": "sentiment_satisfied", - "color": "#2b78c3", - "show_in_apps": true, - "access": { - "groups": ["users"] - } -} diff --git a/packages/core/apps/welcome/source b/packages/core/apps/welcome/source new file mode 160000 index 000000000..a62169378 --- /dev/null +++ b/packages/core/apps/welcome/source @@ -0,0 +1 @@ +Subproject commit a62169378f65ec13e9e1b839271549dd0ea1cd79 diff --git a/packages/core/classes/Log.class.php b/packages/core/classes/Log.class.php index 1e51b6842..7c5b1cc2b 100644 --- a/packages/core/classes/Log.class.php +++ b/packages/core/classes/Log.class.php @@ -1,7 +1,7 @@ - Some Rights Reserved, Cedric Francoys, 2010-2021 + This file is part of the eQual framework + Some Rights Reserved, Cedric Francoys, 2010-2024 Licensed under GNU GPL 3 license */ namespace core; @@ -43,4 +43,12 @@ public static function getColumns() { ]; } -} \ No newline at end of file + + public function getUnique() { + return [ + ['user_id'], + ['object_class'], + ['object_id'] + ]; + } +} diff --git a/packages/core/classes/pipeline/Node.class.php b/packages/core/classes/pipeline/Node.class.php new file mode 100644 index 000000000..be917891d --- /dev/null +++ b/packages/core/classes/pipeline/Node.class.php @@ -0,0 +1,67 @@ + + Some Rights Reserved, Cedric Francoys, 2010-2021 + Licensed under GNU GPL 3 license +*/ + +namespace core\pipeline; + +use equal\orm\Model; + +class Node extends Model +{ + + public static function getColumns() + { + return [ + 'name' => [ + 'type' => 'string', + 'required' => true + ], + + 'description' => [ + 'type' => 'string' + ], + + 'pipeline_id' => [ + 'type' => 'many2one', + 'foreign_object' => 'core\pipeline\Pipeline', + 'required' => true + ], + + 'out_links_ids' => [ + 'type' => 'one2many', + 'foreign_object' => 'core\pipeline\NodeLink', + 'foreign_field' => 'source_node_id' + ], + + 'in_links_ids' => [ + 'type' => 'one2many', + 'foreign_object' => 'core\pipeline\NodeLink', + 'foreign_field' => 'target_node_id' + ], + + 'operation_controller' => [ + 'type' => 'string' + ], + + 'operation_type' => [ + 'type' => 'string' + ], + + 'params_ids' => [ + 'type' => 'one2many', + 'foreign_object' => 'core\pipeline\Parameter', + 'foreign_field' => 'node_id' + ] + ]; + } + + public function getUnique() + { + return [ + ['name', 'pipeline_id'] + ]; + } +} diff --git a/packages/core/classes/pipeline/NodeLink.class.php b/packages/core/classes/pipeline/NodeLink.class.php new file mode 100644 index 000000000..551c7c82e --- /dev/null +++ b/packages/core/classes/pipeline/NodeLink.class.php @@ -0,0 +1,40 @@ + + Some Rights Reserved, Cedric Francoys, 2010-2021 + Licensed under GNU GPL 3 license +*/ + +namespace core\pipeline; + +use equal\orm\Model; + +class NodeLink extends Model +{ + + public static function getColumns() + { + return [ + 'reference_node_id' => [ + 'type' => 'integer', + 'required' => true + ], + + 'source_node_id' => [ + 'type' => 'many2one', + 'foreign_object' => 'core\pipeline\Node', + 'required' => true + ], + + 'target_node_id' => [ + 'type' => 'many2one', + 'foreign_object' => 'core\pipeline\Node', + 'required' => true + ], + + 'target_param' => [ + 'type' => 'string' + ] + ]; + } +} diff --git a/packages/core/classes/pipeline/Parameter.class.php b/packages/core/classes/pipeline/Parameter.class.php new file mode 100644 index 000000000..29ee3d10b --- /dev/null +++ b/packages/core/classes/pipeline/Parameter.class.php @@ -0,0 +1,34 @@ + + Some Rights Reserved, Yesbabylon SRL, 2020-2021 + Licensed under GNU AGPL 3 license +*/ + +namespace core\pipeline; + +use equal\orm\Model; + +class Parameter extends Model +{ + + public static function getColumns() + { + return [ + 'node_id' => [ + 'type' => 'many2one', + 'foreign_object' => 'core\pipeline\Node' + ], + + 'value' => [ + 'type' => 'string', + 'required' => true + ], + + 'param' => [ + 'type' => 'string', + 'required' => true + ] + ]; + } +} diff --git a/packages/core/classes/pipeline/Pipeline.class.php b/packages/core/classes/pipeline/Pipeline.class.php new file mode 100644 index 000000000..08fdafbde --- /dev/null +++ b/packages/core/classes/pipeline/Pipeline.class.php @@ -0,0 +1,31 @@ + + Some Rights Reserved, Cedric Francoys, 2010-2021 + Licensed under GNU GPL 3 license +*/ + +namespace core\pipeline; + +use equal\orm\Model; + +class Pipeline extends Model +{ + + public static function getColumns() + { + return [ + 'nodes_ids' => [ + 'type' => 'one2many', + 'foreign_object' => 'core\pipeline\Node', + 'foreign_field' => 'pipeline_id' + ], + + 'name' => [ + 'type' => 'string', + 'required' => true, + 'unique' => true + ], + ]; + } +} diff --git a/packages/core/classes/pipeline/PipelineExecution.class.php b/packages/core/classes/pipeline/PipelineExecution.class.php new file mode 100644 index 000000000..b4e03b80b --- /dev/null +++ b/packages/core/classes/pipeline/PipelineExecution.class.php @@ -0,0 +1,30 @@ + + Some Rights Reserved, Cedric Francoys, 2010-2021 + Licensed under GNU GPL 3 license +*/ + +namespace core\pipeline; + +use equal\orm\Model; + +class PipelineExecution extends Model +{ + + public static function getColumns() + { + return [ + + 'pipeline_id' => [ + 'type' => 'many2one', + 'foreign_object' => 'core\pipeline\Pipeline', + 'required' => true + ], + + 'status' => [ + 'type' => 'string' + ] + ]; + } +} diff --git a/packages/core/classes/pipeline/PipelineNodeExecution.class.php b/packages/core/classes/pipeline/PipelineNodeExecution.class.php new file mode 100644 index 000000000..2264b70e5 --- /dev/null +++ b/packages/core/classes/pipeline/PipelineNodeExecution.class.php @@ -0,0 +1,40 @@ + + Some Rights Reserved, Cedric Francoys, 2010-2021 + Licensed under GNU GPL 3 license +*/ + +namespace core\pipeline; + +use equal\orm\Model; + +class PipelineNodeExecution extends Model +{ + + public static function getColumns() + { + return [ + + 'pipeline_execution_id' => [ + 'type' => 'many2one', + 'foreign_object' => 'core\pipeline\PipelineExecution', + 'required' => true + ], + + 'node_id' => [ + 'type' => 'many2one', + 'foreign_object' => 'core\pipeline\Node', + 'required' => true + ], + + 'status' => [ + 'type' => 'string' + ], + + 'result' => [ + 'type' => 'string' + ] + ]; + } +} diff --git a/packages/core/classes/test/Test.class.php b/packages/core/classes/test/Test.class.php index 8cc22a013..5892cefb2 100644 --- a/packages/core/classes/test/Test.class.php +++ b/packages/core/classes/test/Test.class.php @@ -16,6 +16,10 @@ */ class Test extends Model { + public static function getDescription() { + return "This Class is defined for testing purpose, and is intended to be used in testing units of the core package."; + } + public static function getColumns() { return [ diff --git a/packages/core/data/check-pipeline.php b/packages/core/data/check-pipeline.php new file mode 100644 index 000000000..03b41322b --- /dev/null +++ b/packages/core/data/check-pipeline.php @@ -0,0 +1,96 @@ + + Some Rights Reserved, Cedric Francoys, 2010-2021 + Licensed under GNU LGPL 3 license +*/ + +use core\pipeline\Pipeline; + +list($params, $providers) = eQual::announce([ + 'description' => 'Run the given pipeline.', + 'params' => [ + 'pipeline_id' => [ + 'description' => 'Pipeline\'s id', + 'type' => 'integer' + ] + ], + 'response' => [ + 'content-type' => 'application/json', + 'charset' => 'UTF-8', + 'accept-origin' => '*', + 'schema' => [ + 'type' => '', + 'qty' => '' + ] + ], + 'access' => [ + 'visibility' => 'protected' + ], + 'providers' => ['context'] +]); + +/** + * @var \equal\php\Context $context + */ +$context = $providers['context']; + +$pipeline = Pipeline::id($params['pipeline_id']) + ->read([ + 'nodes_ids' => [ + 'id', + 'in_links_ids' => ['source_node_id'], + 'out_links_ids' => ['target_node_id'] + ] + ]) + ->first(); + +$pipeline_nodes = $pipeline['nodes_ids']->get(true); + +$graph = []; + +foreach ($pipeline_nodes as $node) { + $graph[$node['id']] = []; + foreach ($node['in_links_ids'] as $link) { + $graph[$node['id']][] = $link['source_node_id']; + } + foreach ($node['out_links_ids'] as $link) { + $graph[$node['id']][] = $link['target_node_id']; + } + $graph[$node['id']] = array_unique($graph[$node['id']]); +} + +if (!isGraphConnected($graph)) { + throw new Exception('non-compliant_pipeline', QN_ERROR_UNKNOWN); +} + +$context->httpResponse() + ->body(['success' => true]) + ->send(); + +function isGraphConnected($graph) +{ + $visited = []; + $start_node = array_key_first($graph); + + depthSearch($graph, $start_node, $visited); + + foreach ($graph as $node => $adjacent_nodes) { + if (!isset($visited[$node])) { + return false; + } + } + + return true; +}; + +function depthSearch($graph, $node, &$visited) +{ + $visited[$node] = true; + + foreach ($graph[$node] as $adjacent_node) { + if (!isset($visited[$adjacent_node])) { + depthSearch($graph, $adjacent_node, $visited); + } + } +}; diff --git a/packages/core/data/model/schema.php b/packages/core/data/model/schema.php index c04db44ef..7896a7ec4 100644 --- a/packages/core/data/model/schema.php +++ b/packages/core/data/model/schema.php @@ -1,6 +1,6 @@ + This file is part of the eQual framework Some Rights Reserved, Cedric Francoys, 2010-2021 Licensed under GNU LGPL 3 license */ diff --git a/packages/core/data/model/view.php b/packages/core/data/model/view.php index ddc7f1d03..2ed1514b7 100644 --- a/packages/core/data/model/view.php +++ b/packages/core/data/model/view.php @@ -29,27 +29,37 @@ list($context, $orm) = [$providers['context'], $providers['orm']]; $removeNodes = function (&$layout, $nodes_ids) { - foreach($layout['groups'] ?? [] as $group_index => $group) { + $groups = $layout['groups'] ?? []; + for($group_index = count($groups) - 1; $group_index >= 0; --$group_index) { + $group = $groups[$group_index]; 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) { + $sections = $group['sections'] ?? []; + for($section_index = count($sections) - 1; $section_index >= 0; --$section_index) { + $section = $sections[$section_index]; 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) { + $rows = $section['rows'] ?? []; + for($row_index = count($rows) - 1; $row_index >= 0; --$row_index) { + $row = $rows[$row_index]; 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) { + $columns = $row['columns'] ?? []; + for($column_index = count($columns) - 1; $column_index >= 0; --$column_index) { + $column = $columns[$column_index]; 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($column['items'] ?? [] as $item_index => $item) { + $items = $column['items'] ?? []; + for($item_index = count($items) - 1; $item_index >= 0; --$item_index) { + $item = $items[$item_index]; 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; @@ -59,8 +69,10 @@ } } } - - foreach($layout['items'] ?? [] as $item_index => $item) { + // handle case where items are directly defined in the layout + $items = $layout['items'] ?? []; + for($item_index = count($items) - 1; $item_index >= 0; --$item_index) { + $item = $items[$item_index]; if(isset($item['id']) && in_array($item['id'], $nodes_ids)) { array_splice($layout['items'], $item_index, 1); } @@ -69,6 +81,7 @@ $updateNode = function (&$layout, $id, $node) { $target = null; + $target_type = ''; $index = 0; $target_parent = null; foreach($layout['groups'] as $group_index => $group) { @@ -80,6 +93,7 @@ foreach($group['sections'] as $section_index => $section) { if(isset($section['id']) && $section['id'] == $id) { $target = &$layout['groups'][$group_index]['sections'][$section_index]; + $target_type = 'section'; $index = $section_index; break 2; } @@ -87,6 +101,7 @@ 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]; + $target_type = 'row'; $index = $row_index; break 3; } @@ -94,6 +109,7 @@ 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]; + $target_type = 'column'; $index = $column_index; break 4; } @@ -101,6 +117,7 @@ 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]; + $target_type = 'item'; $index = $item_index; break 5; } @@ -128,12 +145,22 @@ } if(isset($node['prepend'])) { foreach((array) $node['prepend'] as $elem) { - array_unshift($target, $elem); + if($target_type == 'column') { + array_unshift($target['items'], $elem); + } + else { + array_unshift($target, $elem); + } } } if(isset($node['append'])) { foreach((array) $node['append'] as $elem) { - array_push($target, $elem); + if($target_type == 'column') { + array_push($target['items'], $elem); + } + else { + array_push($target, $elem); + } } } if($target_parent) { diff --git a/packages/core/data/pipeline/test-divide.php b/packages/core/data/pipeline/test-divide.php new file mode 100644 index 000000000..d4dedfb27 --- /dev/null +++ b/packages/core/data/pipeline/test-divide.php @@ -0,0 +1,49 @@ + + Some Rights Reserved, Cedric Francoys, 2010-2021 + Licensed under GNU LGPL 3 license +*/ +list($params, $providers) = eQual::announce([ + 'description' => 'Returns the division of two values.', + 'params' => [ + 'numerator' => [ + 'description' => 'Numerator', + 'type' => 'integer', + 'usage' => 'numeric/integer', + 'required' => true + ], + 'denominator' => [ + 'description' => 'Denominator', + 'type' => 'integer', + 'usage' => 'numeric/integer', + 'required' => true + ] + ], + 'response' => [ + 'content-type' => 'application/json', + 'charset' => 'UTF-8', + 'accept-origin' => '*', + 'schema' => [ + 'type' => 'integer', + 'usage' => 'numeric/integer', + 'qty' => 'one' + ] + ], + 'access' => [ + 'visibility' => 'public', + ], + 'providers' => ['context'] +]); + +list($context) = [$providers['context']]; + +$result = 0; + +if ($params['denominator'] != 0) { + $result = intdiv($params['numerator'], $params['denominator']); +} + +$context->httpResponse() + ->body($result) + ->send(); diff --git a/packages/core/data/pipeline/test-sum-list.php b/packages/core/data/pipeline/test-sum-list.php new file mode 100644 index 000000000..f3bded8d3 --- /dev/null +++ b/packages/core/data/pipeline/test-sum-list.php @@ -0,0 +1,42 @@ + + Some Rights Reserved, Cedric Francoys, 2010-2021 + Licensed under GNU LGPL 3 license +*/ +list($params, $providers) = eQual::announce([ + 'description' => 'Returns the sum of a list of integer', + 'params' => [ + 'list' => [ + 'description' => 'list of integer', + 'type' => 'array', + 'required' => true + ] + ], + 'response' => [ + 'content-type' => 'application/json', + 'charset' => 'UTF-8', + 'accept-origin' => '*', + 'schema' => [ + 'type' => 'integer', + 'usage' => 'numeric/integer', + 'qty' => 'one' + ] + ], + 'access' => [ + 'visibility' => 'public', + ], + 'providers' => ['context'] +]); + +list($context) = [$providers['context']]; + +$result = 0; + +foreach ($params['list'] as $element) { + $result += $element; +} + +$context->httpResponse() + ->body($result) + ->send(); diff --git a/packages/core/data/pipeline/test-sum.php b/packages/core/data/pipeline/test-sum.php new file mode 100644 index 000000000..3960bd18e --- /dev/null +++ b/packages/core/data/pipeline/test-sum.php @@ -0,0 +1,45 @@ + + Some Rights Reserved, Cedric Francoys, 2010-2021 + Licensed under GNU LGPL 3 license +*/ +list($params, $providers) = eQual::announce([ + 'description' => 'Returns the sum of two values.', + 'params' => [ + 'first_value' => [ + 'description' => 'First value', + 'type' => 'integer', + 'usage' => 'numeric/integer', + 'required' => true + ], + 'second_value' => [ + 'description' => 'Second value', + 'type' => 'integer', + 'usage' => 'numeric/integer', + 'required' => true + ] + ], + 'response' => [ + 'content-type' => 'application/json', + 'charset' => 'UTF-8', + 'accept-origin' => '*', + 'schema' => [ + 'type' => 'integer', + 'usage' => 'numeric/integer', + 'qty' => 'one' + ] + ], + 'access' => [ + 'visibility' => 'public', + ], + 'providers' => ['context'] +]); + +list($context) = [$providers['context']]; + +$result = $params['first_value'] + $params['second_value']; + +$context->httpResponse() + ->body($result) + ->send(); diff --git a/packages/core/data/run-pipeline.php b/packages/core/data/run-pipeline.php new file mode 100644 index 000000000..8d929a24e --- /dev/null +++ b/packages/core/data/run-pipeline.php @@ -0,0 +1,107 @@ + + Some Rights Reserved, Cedric Francoys, 2010-2021 + Licensed under GNU LGPL 3 license +*/ + +use core\pipeline\Pipeline; + +list($params, $providers) = eQual::announce([ + 'description' => 'Run the given pipeline.', + 'params' => [ + 'pipeline_id' => [ + 'description' => 'Pipeline\'s id', + 'type' => 'integer' + ] + ], + 'response' => [ + 'content-type' => 'application/json', + 'charset' => 'UTF-8', + 'accept-origin' => '*', + 'schema' => [ + 'type' => '', + 'qty' => '' + ] + ], + 'access' => [ + 'visibility' => 'protected' + ], + 'providers' => ['context'] +]); + +/** + * @var \equal\php\Context $context + */ +$context = $providers['context']; + +eQual::run("get", "core_check-pipeline", $params, true); + +$pipeline = Pipeline::id($params['pipeline_id']) + ->read([ + 'nodes_ids' => [ + 'id', + 'name', + 'operation_controller', + 'operation_type', + 'in_links_ids' => ['reference_node_id', 'source_node_id', 'target_param'], + 'out_links_ids' => ['target_node_id'], + 'params_ids' => ['value', 'param'] + ] + ]) + ->first(); + +$pipeline_nodes = $pipeline['nodes_ids']->get(true); + +$count = 0; + +$result_map = $name_map = []; + +foreach ($pipeline_nodes as $node) { + if ($node['operation_type'] != null) { + $result_map[$node['id']] = null; + $name_map[$node['id']] = $node['name']; + if (empty($node['in_links_ids'])) { + $parameters = []; + foreach ($node['params_ids'] as $param) { + $parameters[$param['param']] = json_decode($param['value']); + } + $result_map[$node['id']] = eQual::run($node['operation_type'], $node['operation_controller'], $parameters, true); + $count++; + } + } +} + +while ($count != count($result_map)) { + foreach ($pipeline_nodes as $node) { + if ($node['operation_type'] != null && $result_map[$node['id']] == null) { + $is_computable = true; + $parameters = []; + foreach ($node['in_links_ids'] as $link) { + if ($result_map[$link['reference_node_id']] != null) { + $parameters[$link['target_param']] = $result_map[$link['reference_node_id']]; + } else { + $is_computable = false; + break; + } + } + if ($is_computable) { + foreach ($node['params_ids'] as $param) { + $parameters[$param['param']] = json_decode($param['value']); + } + $result_map[$node['id']] = eQual::run($node['operation_type'], $node['operation_controller'], $parameters, true); + $count++; + } + } + } +} + +$res = []; + +foreach ($name_map as $key => $value) { + $res[$value] = $result_map[$key]; +} + +$context->httpResponse() + ->body($res) + ->send(); diff --git a/packages/core/data/userinfo.php b/packages/core/data/userinfo.php index 99e23c4ec..65bf4a4e3 100644 --- a/packages/core/data/userinfo.php +++ b/packages/core/data/userinfo.php @@ -1,50 +1,67 @@ - Some Rights Reserved, Cedric Francoys, 2010-2021 + Some Rights Reserved, Cedric Francoys, 2010-2024 Licensed under GNU LGPL 3 license */ use core\User; -list($params, $providers) = announce([ +list($params, $providers) = eQual::announce([ 'description' => 'Returns descriptor of current User, based on received access_token', + 'constants' => ['AUTH_ACCESS_TOKEN_VALIDITY', 'BACKEND_URL'], 'response' => [ 'content-type' => 'application/json', 'charset' => 'UTF-8', 'accept-origin' => '*' ], - 'providers' => ['context', 'orm', 'auth'] + 'providers' => ['context', 'auth'] ]); // use equal\orm\ObjectManager /** * @var \equal\php\Context $context * @var \equal\auth\AuthenticationManager $auth - * @var \equal\orm\ObjectManager $om */ -list($context, $om, $auth) = [$providers['context'], $providers['orm'], $providers['auth']]; +list($context, $auth) = [$providers['context'], $providers['auth']]; // retrieve current User identifier (HTTP headers lookup through Authentication Manager) $user_id = $auth->userId(); + // make sure user is authenticated if($user_id <= 0) { - throw new Exception('user_unknown', QN_ERROR_INVALID_USER); -} -// request directly the mapper to bypass permission check on User class -$ids = $om->search('core\User', ['id', '=', $user_id]); -// make sure the User object is available -if(!count($ids)) { - throw new Exception('unexpected_error', QN_ERROR_INVALID_USER); + throw new Exception('user_unknown', EQ_ERROR_INVALID_USER); } + // #memo - user has always READ right on its own object -$user = User::ids($ids) - ->read(['id', 'name', 'login', 'language', 'groups_ids' => ['name']]) +$user = User::id($user_id) + ->read([ + 'id', 'name', 'login', 'validated', 'language', + 'groups_ids' => ['name', 'display_name'] + ]) ->adapt('json') ->first(true); +// make sure the User object is available +if(!$user) { + throw new Exception('unexpected_error', EQ_ERROR_INVALID_USER); +} + +if(!$user['validated']) { + throw new Exception("user_not_validated", EQ_ERROR_NOT_ALLOWED); +} + $user['groups'] = array_values(array_map(function ($a) {return $a['name'];}, $user['groups_ids'])); +// renew JWT access token +$access_token = $auth->token($user_id, constant('AUTH_ACCESS_TOKEN_VALIDITY')); + // send back basic info of the User object $context->httpResponse() - ->body($user) - ->send(); + ->cookie('access_token', $access_token, [ + 'expires' => time() + constant('AUTH_ACCESS_TOKEN_VALIDITY'), + 'httponly' => true, + 'secure' => constant('AUTH_TOKEN_HTTPS'), + 'domain' => parse_url(constant('BACKEND_URL'), PHP_URL_HOST) + ]) + ->body($user) + ->send(); diff --git a/packages/core/data/utils/sql-schema.php b/packages/core/data/utils/sql-schema.php index 268bc291d..1b159c32d 100644 --- a/packages/core/data/utils/sql-schema.php +++ b/packages/core/data/utils/sql-schema.php @@ -20,7 +20,7 @@ 'required' => true ], 'full' => [ - 'description' => 'Force the output to complete schema (i.e. with tables already present in DB).', + 'description' => 'Force the output to complete schema (i.e. all tables with all columns, even if already present in DB).', 'type' => 'boolean', 'default' => false ] @@ -112,14 +112,11 @@ 'null' => true ]; - // if a SQL type is associated to field 'usage', it prevails over the type association - // #todo + // #todo - if a SQL type is associated to field 'usage', it prevails over the type association if(isset($description['usage']) && isset(ObjectManager::$usages_associations[$description['usage']])) { // $type = ObjectManager::$usages_associations[$description['usage']]; } - // #memo - default is supported by ORM, not DBMS - if($field == 'id') { continue; // #memo - id column is added at table creation (auto_increment + primary key) @@ -129,6 +126,12 @@ } // generate SQL for column creation $result[] = $db->getQueryAddColumn($table, $field, $column_descriptor); + + // #memo - default is supported and handled by the ORM, not by the DBMS + // if table already exists, set column value according to default, for all existing records + if(count($columns) && isset($description['default'])) { + $result[] = $db->getQuerySetRecords($table, [$field => $description['default']]); + } } elseif($description['type'] == 'computed') { if(!isset($description['store']) || !$description['store']) { @@ -150,10 +153,26 @@ } if(method_exists($model, 'getUnique')) { - // #memo - Classes are allowed to override the getUnique method from their parent class. - // Therefore, we cannot apply parent uniqueness constraints on parent table since it would also applies on all inherited classes. + // #memo - Classes are allowed to override the getUnique method from their parent class. Unique checks are performed by ORM. + // So we cannot apply parent uniqueness constraints on parent table since it would also applies on all inherited classes. + // However, even if check is made by ORM, each column member of a unique tuple must be indexed (for performance concerns). + + $constraints = (array) $model->getUnique(); + $map_index_fields = []; + foreach($constraints as $uniques) { + foreach((array) $uniques as $unique) { + if(isset($schema[$unique])) { + $map_index_fields[$unique] = true; + } + } + } + foreach($map_index_fields as $unique_field => $flag) { + // create an index for fields not yet present in DB + if(!in_array($unique_field, $columns)) { + $result[] = $db->getQueryAddIndex($table, $unique_field); + } + } } - } foreach($m2m_tables as $table => $columns) { @@ -181,7 +200,7 @@ ]); $processed_columns[$table][$column] = true; } - $result[] = $db->getQueryAddConstraint($table, $columns); + $result[] = $db->getQueryAddUniqueConstraint($table, $columns); // add an empty record (required for JOIN conditions on empty tables) $result[] = $db->getQueryAddRecords($table, $columns, [array_fill(0, count($columns), 0)]); } diff --git a/packages/core/views/Permission.form.default.json b/packages/core/views/Permission.form.default.json index a9e7307c1..099c96492 100644 --- a/packages/core/views/Permission.form.default.json +++ b/packages/core/views/Permission.form.default.json @@ -61,7 +61,10 @@ { "type": "field", "value": "user_id", - "width": "100%" + "width": "100%", + "widget": { + "limit": 100 + } } ] } diff --git a/packages/core/views/menu.settings.left.json b/packages/core/views/menu.settings.left.json index d334e3003..04cf5c26d 100644 --- a/packages/core/views/menu.settings.left.json +++ b/packages/core/views/menu.settings.left.json @@ -56,55 +56,6 @@ } } ] - }, - { - "id": "identities", - "label": "Identities", - "description": "", - "icon": "person", - "type": "parent", - "children": [ - { - "id": "identities.identities", - "type": "entry", - "label": "Identities", - "description": "", - "context": { - "entity": "identity\\Identity", - "view": "list.default" - } - }, - { - "id": "identities.partners", - "type": "entry", - "label": "Partners", - "description": "", - "context": { - "entity": "identity\\Partner", - "view": "list.default" - } - }, - { - "id": "identities.addresses", - "type": "entry", - "label": "Addresses", - "description": "", - "context": { - "entity": "identity\\Address", - "view": "list.default" - } - }, - { - "id": "identities.establishments", - "type": "entry", - "label": "Establishments", - "description": "", - "context": { - "entity": "identity\\Establishment", - "view": "list.default" - } - } - ] } ] }, diff --git a/public/assets/img/symbiose_logo_txt_full.png b/public/assets/img/symbiose_logo_txt_full.png deleted file mode 100644 index ecdf43ca4..000000000 Binary files a/public/assets/img/symbiose_logo_txt_full.png and /dev/null differ diff --git a/public/welcome/index.html b/public/welcome/index.html index 8f00a1507..2925db4bc 100644 --- a/public/welcome/index.html +++ b/public/welcome/index.html @@ -136,7 +136,7 @@