diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d212220db..149e01d3f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -50,7 +50,7 @@ Pull requests are the best way to propose changes to the codebase (we use [Githu 6. Create a new Pull Request ### Guidelines -* Before opening a PR for additions or changes, please discuss those by filing an issue on [GitHub](https://github.com/equalframework/equal/issues) or asking about it on [Discord](https://discord.gg/BNCPYxD9kk) (#general channel). This will save you development time by getting feedback upfront and make review faster by giving the maintainers more context and details. +* Before opening a PR for additions or changes, please discuss those by filing an issue on [GitHub](https://github.com/equalframework/equal/issues) or asking about it on [Discord](https://discord.gg/xNAXyhbYBp) (#general channel). This will save you development time by getting feedback upfront and make review faster by giving the maintainers more context and details. * Before submitting a PR, ensure that the code works with all PHP versions that we support (currently PHP 7.0 to PHP 7.4); that the test suite passes and that your code lints. * If you've changed some behavior, update the 'description' and 'help' attributes (when present). * If you are going to submit a pull request, please fork from `master`, and submit your pull request back as a fix/feature branch referencing the GitHub issue number @@ -88,4 +88,4 @@ When writing Unit Tests, please * If you change any global settings, make sure that you reset to the default in the `rollback()`. * Don't over-complicate test code by testing several behaviors in the same test. -This makes it easier to see exactly what is being tested when reviewing the PR. I want to be able to see it in the PR, not have to hunt in other unchanged classes to see what the test is doing. +This makes it easier to see exactly what is being tested when reviewing the PR. We want to be able to see it in the PR, without having to hunt in other unchanged classes to see what the test is doing. diff --git a/README.md b/README.md index 3e5430e41..215869460 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,7 @@ This mechanism enables eQual to generate an application without writing a single ## Features -Beside its revolutionary edge, eQal is a fully-featured framework providing an amazing set of both traditional and innovative features. +Beside its revolutionary edge, eQual is a fully-featured framework providing an amazing set of both traditional and innovative features. ### Low-Code @@ -56,12 +56,12 @@ Beside its revolutionary edge, eQal is a fully-featured framework providing an a ### Architecture :white_check_mark: **CQRS architecture**: Division of controllers into Action Handlers, Data Providers, and App Providers. -:white_check_mark: **MVC segregation**: Strict distinction between Models (entites), Views and Controllers. +:white_check_mark: **MVC segregation**: Strict distinction between Models (entities), Views and Controllers. :white_check_mark: **Dependency injection**: Inject services into classes, methods, controllers, or functions anywhere. :white_check_mark: **Data Adaptation**: Automatically transform received data based on format and context. :white_check_mark: **Services Extensibility**: Ability to extend services behavior, and to register custom ones. :white_check_mark: **Cascading Configuration**: Overridable settings at different levels (default, global, package). -:white_check_mark: **I/O as HTTP messages**: Inputs & outputs are handled as text in all contexts (default is 'aplication/json'). +:white_check_mark: **I/O as HTTP messages**: Inputs & outputs are handled as text in all contexts (default is 'application/json'). ### Entities & ORM :white_check_mark: **Model definition**: With support for inheritance, workflows, actions, roles and policies, transitions, and events. @@ -82,7 +82,7 @@ Beside its revolutionary edge, eQal is a fully-featured framework providing an a :white_check_mark: **Multi-formats exports**: Export any Controller data in TXT, CSV, or PDF formats. ### Miscellaneous -:white_check_mark: **Database**: Ablity to connect to various data source (DBMS), with guidelines to develop your own if needed. +:white_check_mark: **Database**: Ability to connect to various data source (DBMS), with guidelines to develop your own if needed. :white_check_mark: **Multi-user**: Allows concurrency through Optimistic Concurrency Control. :white_check_mark: **Multi-lang & Multi-locale support**: Provides multilingual support and regional settings. :white_check_mark: **Self-host**: Supports Docker, Kubernetes, AWS EC2, Google Cloud Run, and more. @@ -121,9 +121,9 @@ For a comprehensive documentation that includes examples and step-by-step instru ## Requirements eQual requires the following environment: -* **PHP 7+** with extensions mysqli (mandatory) + gd opcache zip tidy (optional) +* **PHP 8+** with extensions (opcache, zip, tidy, ...) * **Apache 2+** or **Nginx** -* **MySQL 5+** compatible DBMS (tested up to MySQL 5.7 and MariaDB 10.3) +* **SQL:2011** compatible DBMS (SQLite, MariaDB Server, Microsoft SQL Server, Oracle MySQL) ## Install @@ -159,4 +159,4 @@ For questions or any type of support, you can reach out via [Discord](https://di ## License -eQual framework project - Released under the GNU Lesser General Public License v3.0. \ No newline at end of file +eQual framework project - Released under the [GNU Lesser General Public License v3.0](https://www.gnu.org/licenses/lgpl-3.0). \ No newline at end of file diff --git a/config/schema.json b/config/schema.json index d6247c777..87903c19c 100644 --- a/config/schema.json +++ b/config/schema.json @@ -256,6 +256,20 @@ "instant": true, "default": "DB" }, + "HTTP_TRUSTED_HEADERS": { + "type": "array", + "usage": "text/plain[]", + "description": "Specific HTTP headers that are used to pass information from a proxy or load balancer to the backend server.", + "instant": true, + "default": [] + }, + "HTTP_TRUSTED_PROXIES": { + "type": "array", + "usage": "ipv4/ddn[]", + "description": "Specific proxy servers or load balancers that an application or server is configured to trust when handling incoming request.", + "instant": true, + "default": [] + }, "HTTP_REQUEST_TIMEOUT": { "type": "integer", "usage": "time/duration", diff --git a/doc b/doc index a50423244..4da653845 160000 --- a/doc +++ b/doc @@ -1 +1 @@ -Subproject commit a504232442ffb7acc2c669b7cab848e134978c88 +Subproject commit 4da653845d41bb6aaf99170caf4aaaef80021a74 diff --git a/lib/equal/data/DataValidator.class.php b/lib/equal/data/DataValidator.class.php index 6e82c025a..0e8635dc5 100644 --- a/lib/equal/data/DataValidator.class.php +++ b/lib/equal/data/DataValidator.class.php @@ -263,7 +263,7 @@ public function checkConstraints(Field $field, $value) { if(!isset($constraint['message'])) { $constraint['message'] = 'Invalid field.'; } - trigger_error("ORM::given value for field `{$field}` violates constraint : {$constraint['message']}", QN_REPORT_INFO); + trigger_error("ORM::given value for field `{$field}` violates constraint `{$error_id}`: {$constraint['message']}", EQ_REPORT_WARNING); $result[$error_id] = $constraint['message']; } } diff --git a/lib/equal/orm/Field.class.php b/lib/equal/orm/Field.class.php index a5b922a0a..f3896eb7d 100644 --- a/lib/equal/orm/Field.class.php +++ b/lib/equal/orm/Field.class.php @@ -73,6 +73,7 @@ protected function getUsageString(): string { 'datetime' => 'date/time', 'time' => 'time/plain', 'binary' => 'binary/plain:16000000', + 'file' => 'binary/plain:16000000', 'many2one' => 'number/integer:9', 'one2many' => 'array', 'many2many' => 'array', @@ -99,6 +100,7 @@ public function getContentType(): string { 'datetime' => 'date/datetime', 'time' => 'time/plain', 'binary' => 'binary/plain', + 'file' => 'binary/plain', 'many2one' => 'number/natural', 'one2many' => 'array', 'many2many' => 'array', @@ -112,6 +114,7 @@ public function getUsage(): Usage { if(is_null($this->usage)) { // by default, use the usage string for which the field type is an alias $this->usage = UsageFactory::create($this->getUsageString()); + trigger_error("ORM::no usage set for field `{$this->name}`: assigned to `".$this->getUsageString()."`", EQ_REPORT_INFO); } return $this->usage; } diff --git a/lib/equal/orm/UsageFactory.class.php b/lib/equal/orm/UsageFactory.class.php index 268d58adc..02f23dfda 100644 --- a/lib/equal/orm/UsageFactory.class.php +++ b/lib/equal/orm/UsageFactory.class.php @@ -83,12 +83,12 @@ public static function create(string $str_usage): Usage { case 'image': $usageInstance = new UsageBinary($str_usage); break; - /* non-generic content-types */ case 'amount': $usageInstance = new UsageAmount($str_usage); break; case 'coordinate': + // #todo break; case 'country': $usageInstance = new UsageCountry($str_usage); diff --git a/lib/equal/orm/usages/UsageBinary.class.php b/lib/equal/orm/usages/UsageBinary.class.php index ec782da68..f8e3b36da 100644 --- a/lib/equal/orm/usages/UsageBinary.class.php +++ b/lib/equal/orm/usages/UsageBinary.class.php @@ -19,7 +19,7 @@ public function __construct(string $usage_str) { public function getConstraints(): array { return [ 'size_exceeded' => [ - 'message' => 'Image exceeds length constraint.', + 'message' => 'Binary exceeds length constraint.', 'function' => function($value) { $len = intval($this->getLength()); $strlen = strlen($value); diff --git a/lib/equal/php/Context.class.php b/lib/equal/php/Context.class.php index c085e2379..a699b6270 100644 --- a/lib/equal/php/Context.class.php +++ b/lib/equal/php/Context.class.php @@ -48,6 +48,10 @@ public function __toString() { return 'This is the PHP context instance.'; } + public static function constants() { + return ['HTTP_TRUSTED_HEADERS', 'HTTP_TRUSTED_PROXIES']; + } + public function get($var, $default=null) { if(isset($this->params[$var])) { return $this->params[$var]; @@ -240,13 +244,24 @@ private function getHttpRequestHeaders() { if(!isset($headers['ETag'])) { $headers['ETag'] = $headers['If-None-Match'] ?? ''; } - // handle client IP address - // use only REMOTE_ADDR, if present (to prevent spoofing) - fallback to localhost - $headers['X-Forwarded-For'] = '127.0.0.1'; - // #memo - using CLI, REMOTE_ADDR is not set - if(isset($_SERVER['REMOTE_ADDR'])) { - $headers['X-Forwarded-For'] = $_SERVER['REMOTE_ADDR']; + // store client IP address in `X-Forwarded-For` header + // use original `X-Forwarded-For` only if trusted or present in trusted proxies (to prevent spoofing) - fallback on REMOTE_ADDR, or localhost + $ip = '127.0.0.1'; + if(isset($_SERVER['HTTP_X_FORWARDED_FOR']) && in_array('X-Forwarded-For', constant('HTTP_TRUSTED_HEADERS'))) { + $client_ips = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']); + $ip = $_SERVER['REMOTE_ADDR'] ?? '127.0.0.1'; + foreach(array_reverse($client_ips) as $client_ip) { + $client_ip = trim($client_ip); + if(!in_array($client_ip, constant('HTTP_TRUSTED_PROXIES'))) { + $ip = $client_ip; + break; + } + } + } + elseif(isset($_SERVER['REMOTE_ADDR'])) { + $ip = $_SERVER['REMOTE_ADDR']; } + $headers['X-Forwarded-For'] = $ip; } // set default content type to 'application/x-www-form-urlencoded' diff --git a/packages/core/apps/apps/version b/packages/core/apps/apps/version index c8b61c31e..b55c7ced7 100644 --- a/packages/core/apps/apps/version +++ b/packages/core/apps/apps/version @@ -1 +1 @@ -3259247aafc763abdbb52639061536cb +67985d85f939b2734c5510eabc7d253c diff --git a/packages/core/apps/apps/web.app b/packages/core/apps/apps/web.app index 92d26add1..e0873d89d 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/version b/packages/core/apps/auth/version index bc34048ec..d928ddd84 100644 --- a/packages/core/apps/auth/version +++ b/packages/core/apps/auth/version @@ -1 +1 @@ -d6507202322c5ffce6b5c258517d5fd6 +8f177a53b1d9439b24c5bfc5d0c959ef diff --git a/packages/core/apps/auth/web.app b/packages/core/apps/auth/web.app index fc10dcdbd..12554d87b 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 c2ac29e29..cd463dae6 100644 --- a/packages/core/apps/console.php +++ b/packages/core/apps/console.php @@ -192,9 +192,9 @@ $result = []; -if(file_exists(QN_BASEDIR.'/log/eq_error.log')) { +if(file_exists(QN_BASEDIR.'/log/equal.log')) { // read raw data from pointer log file - $fp = fopen(QN_BASEDIR.'/log/eq_error.log', "r"); + $fp = fopen(QN_BASEDIR.'/log/equal.log', "r"); $result[] = "START LOG"; $i = 0; $prev_thread_id = 0; diff --git a/packages/core/apps/settings/source b/packages/core/apps/settings/source index 97359b659..0d1bc6c95 160000 --- a/packages/core/apps/settings/source +++ b/packages/core/apps/settings/source @@ -1 +1 @@ -Subproject commit 97359b659268799c89cf5f4eccc74f683c414545 +Subproject commit 0d1bc6c957ae2e9794036b25e7fa5998ff1d23d8 diff --git a/packages/core/apps/workbench/source b/packages/core/apps/workbench/source index 9c423378d..54497eb1f 160000 --- a/packages/core/apps/workbench/source +++ b/packages/core/apps/workbench/source @@ -1 +1 @@ -Subproject commit 9c423378d4a285baedf4cd912efb1bff5176619c +Subproject commit 54497eb1f9940287369285161471edcdb3916da3 diff --git a/packages/core/apps/workbench/version b/packages/core/apps/workbench/version index 8f8b7e98a..3579b1508 100644 --- a/packages/core/apps/workbench/version +++ b/packages/core/apps/workbench/version @@ -1 +1 @@ -d85e1ea1561f68f47fa57aad8707abec +33d3ea15aca35ca4204c05abdef0d339 diff --git a/packages/core/apps/workbench/web.app b/packages/core/apps/workbench/web.app index 95a7bafd5..e8601c238 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/Mail.class.php b/packages/core/classes/Mail.class.php index 8ec68aeff..21c57d8ee 100644 --- a/packages/core/classes/Mail.class.php +++ b/packages/core/classes/Mail.class.php @@ -353,12 +353,18 @@ private static function createEnvelope($message): \Swift_Message { ->setTo($message['to']) ->setFrom([constant('EMAIL_SMTP_ACCOUNT_EMAIL') => constant('EMAIL_SMTP_ACCOUNT_DISPLAYNAME')]); - if(isset($message['cc']) && strlen($message['cc']) > 0) { - $envelope->setCc($message['cc']); + if(isset($message['cc'])) { + if( (is_array($message['cc']) && count($message['cc']) > 0) + || (is_string($message['cc']) && strlen($message['cc']) > 0) ) { + $envelope->setCc($message['cc']); + } } - if(isset($message['bcc']) && strlen($message['bcc']) > 0) { - $envelope->setBcc($message['bcc']); + if(isset($message['bcc'])) { + if( (is_array($message['bcc']) && count($message['bcc']) > 0) + || (is_string($message['bcc']) && strlen($message['bcc']) > 0) ) { + $envelope->setBcc($message['bcc']); + } } if(isset($message['reply_to']) && strlen($message['reply_to']) > 0) { diff --git a/public/console.php b/public/console.php index cdb2a50e7..95bf3a362 100644 --- a/public/console.php +++ b/public/console.php @@ -210,9 +210,27 @@ width: 50%; } + #header { + position: fixed; + top: 0; + height: 105px; + width: 100%; + background: white; + z-index: 4; + } + + #searchForm { + padding: 25px 20px 15px 20px; + background: #f5f5f5; + margin: 10px; + border: solid 1px #dfdfdf; + border-radius: 15px; + } + #start { - padding-top: 120px; + padding-top: 110px; } + .loader-overlay { display: none; position: relative; @@ -677,8 +695,8 @@ function createTraceElement(trace, i) { -