diff --git a/.docker/Dockerfile b/.docker/Dockerfile
index 98d35a744..e695b1a53 100644
--- a/.docker/Dockerfile
+++ b/.docker/Dockerfile
@@ -12,14 +12,12 @@ RUN apt-get update && apt-get install -y \
vim
COPY ./mpm_prefork.conf /etc/apache2/mods-available/
COPY ./000-default.conf /etc/apache2/sites-available/
-
# install sqlsrv drivers
RUN curl https://packages.microsoft.com/keys/microsoft.asc | apt-key add -
RUN curl https://packages.microsoft.com/config/ubuntu/20.04/prod.list > /etc/apt/sources.list.d/mssql-release.list
RUN apt-get update
RUN ACCEPT_EULA=Y apt-get -y --no-install-recommends install msodbcsql17 odbcinst=2.3.7 odbcinst1debian2=2.3.7 unixodbc-dev=2.3.7 unixodbc=2.3.7
RUN pecl install sqlsrv-5.10.1 pdo_sqlsrv-5.10.1
-
# install required PHP extensions
RUN set -ex; \
\
@@ -38,7 +36,8 @@ RUN set -ex; \
; \
\
docker-php-ext-configure gd --with-freetype --with-jpeg --with-webp; \
- docker-php-ext-install gd mysqli opcache zip tidy; \
+ docker-php-ext-configure intl; \
+ docker-php-ext-install gd mysqli opcache zip tidy intl; \
docker-php-ext-enable sqlsrv pdo_sqlsrv; \
\
# reset apt-mark's "manual" list so that "purge --auto-remove" will remove all build dependencies
diff --git a/.gitignore b/.gitignore
index 175771ad8..e1ca2a9fa 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,5 @@
vendor/
+tests/
log/*
!log/.gitkeep
cache/*
@@ -18,4 +19,5 @@ public/*
!public/index.php
!public/console.php
!public/assets
+!public/console2.php
public/assets/env/config.json
diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 000000000..702f0fa7c
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,6 @@
+[submodule "doc"]
+ path = doc
+ url = https://github.com/equalframework/equal-doc.git
+[submodule "packages/core/apps/workbench/source"]
+ path = packages/core/apps/workbench/source
+ url = https://github.com/equalframework/apps-core-workbench.git
diff --git a/README.md b/README.md
index dac0cb888..47b067b65 100644
--- a/README.md
+++ b/README.md
@@ -52,6 +52,9 @@ git clone https://github.com/equalframework/equal.git
For more info, see : [http://doc.equal.run/getting-started/installation](http://doc.equal.run/getting-started/installation/)
+## CLI autocomplete
+To ease commands typing in CLI, you can enable autocompletion by running `source autocomplete`.
+
## Code Coverage setup
diff --git a/equal_autocomplete b/autocomplete
similarity index 85%
rename from equal_autocomplete
rename to autocomplete
index 89d3c03d7..af0451523 100644
--- a/equal_autocomplete
+++ b/autocomplete
@@ -1,5 +1,5 @@
# this script is meant to be placed at /etc/bash_completion.d/
-# and activated with source /etc/bash_completion.d/equal_autocomplete
+# and activated with `source /etc/bash_completion.d/autocomplete`
_equal_completion()
{
diff --git a/config/schema.json b/config/schema.json
index e550773dd..387b40478 100644
--- a/config/schema.json
+++ b/config/schema.json
@@ -15,7 +15,7 @@
},
"DEFAULT_LANG": {
"type": "string",
- "description": "The language in which the content must be displayed by default (ISO 639-1).",
+ "description": "The language in which the multilang content must be provided/stored by default (ISO 639-1).",
"instant": true,
"default": "en"
},
@@ -78,6 +78,12 @@
"instant": true,
"default": 10
},
+ "HTTP_PROCESS_USERNAME": {
+ "type": "string",
+ "description": "Username used by the HTTP service (for rights checks).",
+ "instant": true,
+ "default": "www-data"
+ },
"EMAIL_SMTP_HOST": {
"type": "string",
"description": "Hostname of the SMTP server to use for sending emails.",
@@ -137,7 +143,7 @@
},
"DB_DBMS": {
"type": "string",
- "description": "Database Management System running on the DB host. Possible values are: 'MYSQL', 'SQLSRV', 'MARIADB', 'POSTGRESQL'",
+ "description": "Database Management System running on the DB host. Possible values are: 'MYSQL', 'SQLSRV', 'MARIADB', 'SQLITE', 'POSTGRESQL'",
"instant": true,
"default": "MYSQL",
"examples": [
@@ -270,6 +276,8 @@
},
"ROOT_APP_URL": {
"type": "string",
+ "deprecated": "Please use APP_URL instead.",
+ "description": "The URL of the root folder (mapped with `./public`), with the scheme but without trailing slash (no path).",
"default": "http://equal.local"
},
"USER_ACCOUNT_DISPLAYNAME": {
@@ -286,5 +294,30 @@
"type": "boolean",
"description": "Flag telling if a new user account needs to be validated before being active.",
"default": true
+ },
+ "ORG_NAME" : {
+ "type": "string",
+ "description": "Name of the organisation to which belongs the instance.",
+ "default": "eQual"
+ },
+ "ORG_URL": {
+ "type": "string",
+ "description": "URL of the official website of the organisation.",
+ "default": "https://equal.run"
+ },
+ "APP_NAME": {
+ "type": "string",
+ "description": "Specific brand name of the application, if any.",
+ "default": "eQual"
+ },
+ "APP_URL": {
+ "type": "string",
+ "description": "The URL of the root folder (mapped with `./public`), with the scheme but without trailing slash (no path).",
+ "default": "http://equal.local"
+ },
+ "APP_LOGO_URL": {
+ "type": "string",
+ "description": "The URL of the image to be displayed in UI as App logo in the top left corner.",
+ "default": "/assets/img/logo.svg"
}
}
\ No newline at end of file
diff --git a/config/usages.json b/config/usages.json
index a0cb851c1..5f72296c9 100644
--- a/config/usages.json
+++ b/config/usages.json
@@ -32,7 +32,8 @@
"xml" : {},
"html" : {},
"markdown" : {},
- "wiki" : {}
+ "wiki" : {},
+ "json" : {}
},
"length_free" : true,
"boundary" : true
diff --git a/console b/console
new file mode 100755
index 000000000..0975bf517
Binary files /dev/null and b/console differ
diff --git a/doc b/doc
new file mode 160000
index 000000000..6f04d0ad5
--- /dev/null
+++ b/doc
@@ -0,0 +1 @@
+Subproject commit 6f04d0ad57feb1c34b31738381ff9da7030dcc2c
diff --git a/ecs.php b/ecs.php
index 86385a9e0..ad3d9395b 100644
--- a/ecs.php
+++ b/ecs.php
@@ -9,18 +9,21 @@
return static function (ECSConfig $ecsConfig): void {
$ecsConfig->paths([__DIR__ . '/src', __DIR__ . '/tests']);
- $ecsConfig->ruleWithConfiguration(ArraySyntaxFixer::class, [
- 'syntax' => 'short',
- ]);
-
$ecsConfig->sets([
// run and fix, one by one
SetList::SPACES,
SetList::ARRAY,
- SetList::DOCBLOCK,
- SetList::SYMPLIFY,
- // SetList::COMMON, // adds declare(strict_types=1)
- SetList::CLEAN_CODE,
+ SetList::DOCBLOCK,
+ // #memo - messes with COMMENTS blocks
+ // SetList::SYMPLIFY,
+ // #memo - adds declare(strict_types=1)
+ // SetList::COMMON,
+ SetList::CLEAN_CODE,
SetList::PSR_12,
]);
+
+ $ecsConfig->ruleWithConfiguration(ArraySyntaxFixer::class, [
+ 'syntax' => 'short',
+ ]);
+
};
diff --git a/eq.lib.php b/eq.lib.php
index 1eb179f87..c2ad543a9 100644
--- a/eq.lib.php
+++ b/eq.lib.php
@@ -33,7 +33,7 @@
/**
* Current version of eQual
*/
- define('QN_VERSION', '1.0.0');
+ define('EQ_VERSION', '2.0.0');
/**
* Root directory of current install
@@ -109,13 +109,15 @@
define('QN_R_WRITE', 4);
define('QN_R_DELETE', 8);
define('QN_R_MANAGE', 16);
+ define('QN_R_ALL', 31);
/* equivalence map for naming migration */
- define('R_CREATE', QN_R_CREATE);
- define('R_READ', QN_R_READ);
- define('R_WRITE', QN_R_WRITE);
- define('R_DELETE', QN_R_DELETE);
- define('R_MANAGE', QN_R_MANAGE);
+ define('EQ_R_CREATE', QN_R_CREATE);
+ define('EQ_R_READ', QN_R_READ);
+ define('EQ_R_WRITE', QN_R_WRITE);
+ define('EQ_R_DELETE', QN_R_DELETE);
+ define('EQ_R_MANAGE', QN_R_MANAGE);
+ define('EQ_R_ALL', QN_R_ALL);
/**
* Built-in Users and Groups
@@ -821,6 +823,7 @@ public static function announce(array $announcement) {
}
+
// 1) check if all required parameters have been received
// build mandatory fields array
@@ -831,23 +834,53 @@ public static function announce(array $announcement) {
}
// if at least one mandatory param is missing
$missing_params = array_values(array_diff($mandatory_params, array_keys($body)));
- if( count($missing_params)
- || isset($body['announce'])
- || $method == 'OPTIONS' ) {
+ 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
// (for public and protected controllers this might be considered as security issue as it may reveals a part of the configuration)
- // no feedback about services
- if(isset($announcement['providers'])) {
- // unset($announcement['providers']);
- }
- // no feedback about constants
- if(isset($announcement['constants'])) {
- // unset($announcement['constants']);
+ // if 'help' is amongst the params and request was made through CLI
+ if(php_sapi_name() == 'cli' && isset($body['help'])) {
+ $operation = $context->get('operation');
+ $help = 'Help about ';
+ $help .= strtoupper($operation['type']).' '.$operation['operation']." :\n\n";
+ $help .= "Description:\n";
+ $help .= $announcement["description"]."\n\n";
+ $help .= "Parameters:\n";
+ foreach($announcement['params'] as $name => $info) {
+ $help .= str_pad("--".$name, 20, ' ', STR_PAD_RIGHT);
+ $required = (isset($info['required']))?'(required)':'';
+ $help .= str_pad($required, 12, ' ');
+ $type = $info['type'].( (isset($info['usage']))?'>'.$info['usage']:'');
+ $help .= str_pad($type, 28, ' ');
+ $help .= $info['description']."\n";
+ }
+ $help.= "\nMore Info :\n";
+ foreach($body as $key => $value) {
+ if(isset($announcement['params'][$key])) {
+ $help.= "--".$key." :\n";
+ if(isset($announcement['params'][$key]['help'])) {
+ $help .= ' help: ';
+ $help .= preg_replace("/ {2,}/", str_pad('', 9, ' '), $announcement['params'][$key]['help'])."\n";
+ }
+ if(isset($announcement['params'][$key]['default'])) {
+ $help .= ' default value: ';
+ $help .= json_encode($announcement['params'][$key]['default'], true)."\n";
+ }
+ if(isset($announcement['params'][$key]['selection'])) {
+ $help .= ' selection: ';
+ $help .= json_encode($announcement['params'][$key]['selection'], true)."\n";
+ }
+ }
+ }
+ $response->status(200)
+ ->header('Content-Type', 'text/plain')
+ ->body($help)
+ ->send();
+ throw new \Exception('', 0);
}
// add announcement to response body
$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') {
- // user asked for the announcement or browser requested fingerprint
$response->status(200)
// allow browser to cache the response for 1 year
->header('Cache-Control', 'max-age=31536000')
@@ -972,7 +1005,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 parameter']]), QN_ERROR_INVALID_PARAM);
+ throw new \Exception(serialize([$invalid_param => [$error_id => "Invalid value {$result[$invalid_param]} for parameter {$invalid_param}."]]), QN_ERROR_INVALID_PARAM);
}
}
@@ -1215,7 +1248,7 @@ public static function run($type, $operation, $body=[], $root=false) {
// restore original HTTP Response headers
// #memo - this is mandatory for integration with other frameworks (i.e. WP) while calling `run()` when (some) headers have already been set
$headers_orig = $context_orig->getHttpResponse()->getHeaders(true);
- if(count($headers_orig)) {
+ if(is_array($headers_orig) && count($headers_orig)) {
header_remove();
foreach($headers_orig as $header => $value) {
header($header.': '.$value);
diff --git a/lib/equal/access/AccessController.class.php b/lib/equal/access/AccessController.class.php
index aca62eef2..b2812f228 100644
--- a/lib/equal/access/AccessController.class.php
+++ b/lib/equal/access/AccessController.class.php
@@ -7,7 +7,6 @@
namespace equal\access;
use equal\orm\ObjectManager;
-use equal\orm\Model;
use equal\organic\Service;
use equal\services\Container;
@@ -40,7 +39,7 @@ public function getUserGroups($user_id) {
if(!isset($this->groupsTable[$user_id])) {
// all users are members of default group (including unidentified users)
$this->groupsTable[$user_id] = [(string) QN_DEFAULT_GROUP_ID];
-
+ /** @var \equal\orm\ObjectManager */
$orm = $this->container->get('orm');
$values = $orm->read('core\User', $user_id, ['groups_ids']);
if($values > 0 && isset($values[$user_id])) {
@@ -69,173 +68,262 @@ public function getGroupUsers($group_id) {
/**
* Retrieve the permissions (ACL) that apply for a given user on a target entity.
+ * This method use the AccessController cache to provide previously requested Rights.
*
* @param int $user_id Identifier of the user for which the permissions are requested.
* @param string $object_class The class for which the rights have to be fetched. This param accepts wildcard notation ('*').
- * @param int[] $object_ids This param is optional. If provided, the method will return the minimum set of permissions that are granted for user on all objects of the collection.
+ * @param int[] $object_ids Optional list of objects identifiers. If provided, the method will return the minimum set of permissions that are granted for user on all objects of the collection.
+ * @param int $operation Optional operation (bit mask of rights) we're looking for. If provided, the method will stop searching for permissions as soon as this value is met. This argument is used only when objects_ids are provided.
*
- * @return int Returns a binary mask made of the resulting rights of the given user on the target entity.
+ * @return int Returns a bit mask made of the resulting rights of the given user on the target collection.
*
*/
- protected function getUserRights($user_id, $object_class, $object_ids=[]) {
+ public function getUserRights($user_id, $object_class, $object_ids=[], $operation=EQ_R_ALL) {
// no matter the ACLs, users are always granted the default permissions
- $user_rights = $this->default_rights;
+ // and root user always has full rights
+ $user_rights = ($user_id == QN_ROOT_USER_ID)?EQ_R_ALL:$this->default_rights;
+ // request for rights based on object_class and specific object_ids
if(count($object_ids)) {
- // root user always has full rights
- if($user_id == QN_ROOT_USER_ID) {
- $user_rights = QN_R_CREATE | QN_R_READ | QN_R_WRITE | QN_R_DELETE | QN_R_MANAGE;
+ if($user_rights < $operation) {
+ $user_rights |= $this->getUserRightsOnClass($user_id, $object_class);
}
- else {
- // get all user's groups
- $groups_ids = $this->getUserGroups($user_id);
-
- // build domain
- $domain = [];
- $domain[] = [ ['class_name', '=', $object_class], ['user_id', '=', $user_id], ['object_id', 'in', $object_ids] ];
- $domain[] = [ ['class_name', '=', '*'], ['user_id', '=', $user_id], ['object_id', 'in', $object_ids] ];
- $domain[] = [ ['class_name', '=', $object_class], ['user_id', '=', $user_id], ['object_id', 'is', null] ];
- $domain[] = [ ['class_name', '=', '*'], ['user_id', '=', $user_id], ['object_id', 'is', null] ];
- if(count($groups_ids)) {
- $domain[] = [ ['class_name', '=', $object_class], ['group_id', 'in', $groups_ids], ['object_id', 'in', $object_ids] ];
- $domain[] = [ ['class_name', '=', '*'], ['group_id', 'in', $groups_ids], ['object_id', 'in', $object_ids] ];
- $domain[] = [ ['class_name', '=', $object_class], ['group_id', 'in', $groups_ids], ['object_id', 'is', null] ];
- $domain[] = [ ['class_name', '=', '*'], ['group_id', 'in', $groups_ids], ['object_id', 'is', null] ];
- }
- $orm = $this->container->get('orm');
- // fetch all ACLs variants and build a map by object_id
- $acl_ids = $orm->search('core\Permission', $domain);
- if(count($acl_ids)) {
- $map_objects_permissions = [];
- // get the user permissions
- $values = $orm->read('core\Permission', $acl_ids, ['object_id', 'rights']);
- foreach($values as $acl_id => $acl) {
- if(!isset($map_objects_permissions[$acl['object_id']])) {
- $map_objects_permissions[$acl['object_id']] = $acl['rights'];
- }
- else {
- $map_objects_permissions[$acl['object_id']] |= $acl['rights'];
- }
- }
- $user_rights = QN_R_CREATE | QN_R_READ | QN_R_WRITE | QN_R_DELETE | QN_R_MANAGE;
- foreach($map_objects_permissions as $object_id => $rights) {
- $user_rights &= $rights;
- }
- }
- // user has at least the default rights
- $user_rights = max($user_rights, $this->default_rights);
+ if($user_rights < $operation) {
+ $user_rights |= $this->getUserRightsOnObjects($user_id, $object_class, $object_ids);
}
}
+ // request for rights based on object_class only
else {
-
// if we did already compute user rights then provide the previous result
if(isset($this->permissionsTable[$user_id][$object_class])) {
$user_rights = $this->permissionsTable[$user_id][$object_class];
}
else {
- // root user always has full rights
- if($user_id == QN_ROOT_USER_ID) {
- $user_rights = QN_R_CREATE | QN_R_READ | QN_R_WRITE | QN_R_DELETE | QN_R_MANAGE;
+ // grant READ on system entities ('core' package)
+ if(ObjectManager::getObjectPackage(ObjectManager::getObjectRootClass($object_class)) == 'core') {
+ $user_rights |= EQ_R_READ;
+ }
+ if(strpos($object_class, '*') === false) {
+ $user_rights |= $this->getUserRightsOnClass($user_id, $object_class);
}
else {
+ $user_rights |= $this->getUserRightsOnWildcard($user_id, $object_class);
+ }
+ if(!isset($this->permissionsTable[$user_id])) {
+ $this->permissionsTable[$user_id] = [];
+ }
+ $this->permissionsTable[$user_id][$object_class] = $user_rights;
+ }
+ }
- // grant READ on system entities ('core' package)
- if(ObjectManager::getObjectPackage(ObjectManager::getObjectRootClass($object_class)) == 'core') {
- $user_rights |= QN_R_READ;
- }
+ return $user_rights;
+ }
- // fetch all ACLs variants
- $acls = $this->getUserAcls($user_id, $object_class);
+ /**
+ * Retrieve ACLs for a given user on a specific class, or parents classes, or subsequent wildcards (but not individual objects).
+ * In case none of the considered ACL exist, 0 is returned (default_rights is not considered).
+ *
+ * @param int $user_id Identifier of targeted user.
+ * @param string $object_class Name of the class for which the ACL are requested.
+ * @param int $operation Optional operation (bit mask of rights) we're looking for. If provided, the method will stop searching for permissions as soon as this value is met.
+ *
+ */
+ private function getUserRightsOnClass($user_id, $object_class, $operation=EQ_R_ALL) {
+ $result = 0;
- if($acls > 0 && count($acls)) {
- // get the user permissions
- foreach($acls as $aid => $acl) {
- $user_rights |= $acl['rights'];
- }
- }
+ // `object_class` should be an explicit class, if not provide default rights
+ if(!class_exists($object_class)) {
+ return 0;
+ }
- // store resulting permissions in cache
- if(!isset($this->permissionsTable[$user_id])) {
- $this->permissionsTable[$user_id] = [];
+ $orm = $this->container->get('orm');
+
+ // get all user's groups
+ $groups_ids = $this->getUserGroups($user_id);
+
+ // 1) lookup for exact match ACL
+
+ // build domain
+ $domain = [];
+ $domain[] = [ ['class_name', '=', $object_class], ['user_id', '=', $user_id], ['object_id', 'is', null] ];
+ if(count($groups_ids)) {
+ $domain[] = [ ['class_name', '=', $object_class], ['group_id', 'in', $groups_ids], ['object_id', 'is', null] ];
+ }
+ $acl_ids = $orm->search('core\Permission', $domain);
+ if($acl_ids > 0 && count($acl_ids)) {
+ $acls = $orm->read('core\Permission', $acl_ids, ['rights']);
+ foreach($acls as $acl_id => $acl) {
+ $result |= $acl['rights'];
+ }
+ }
+
+ // 2) lookup on wildcard with class namespace
+
+ $parts = explode('\\', $object_class);
+ array_pop($parts);
+ array_push($parts, '*');
+ $wildcard = implode('\\', $parts);
+ if($result < $operation) {
+ $result |= $this->getUserRightsOnWildcard($user_id, $wildcard);
+ }
+
+ // if no ACL found, lookup for ACLs set on parent class
+ if($result == 0) {
+ $parent_classes = $orm->getObjectParentsClasses($object_class);
+ if(count($parent_classes)) {
+ $classes = [];
+ $table_name = $orm->getObjectTableName($object_class);
+ foreach($parent_classes as $class) {
+ if($orm->getObjectTableName($class) == $table_name) {
+ $classes[] = $class;
+ }
+ }
+ foreach($classes as $class) {
+ $result |= $this->getUserRightsOnClass($user_id, $class);
+ if($result >= $operation) {
+ break;
}
}
- $this->permissionsTable[$user_id][$object_class] = $user_rights;
}
}
- return $user_rights;
+ return $result;
}
-
/**
- * Retrieve ACLs for a given user on whole class or parents classes, but not individual objects.
+ * Retrieves exclusively the rights explicitly set on the given wildcard and subsequent wildcards (core\settings\* -> core\* -> *)
+ * In case neither given wildcard nor subsequent wildcard match any ACL, 0 is returned (default_rights is not considered).
+ *
+ * @param int $user_id Identifier of targeted user.
+ * @param string $object_class Name of the class for which the ACL are requested.
*
- * @param $user_id integer Identifier of targeted user.
- * @param $object_class string Name of the class for which the ACL are requested.
*/
- protected function getUserAcls($user_id, $object_class) {
- $result = [];
+ private function getUserRightsOnWildcard($user_id, $wildcard) {
+ $result = 0;
+
+ // `object_class` should be an explicit class, if not provide default rights
+ if(strpos($wildcard, '*') === false) {
+ return $result;
+ }
$orm = $this->container->get('orm');
- // get user groups
+ // get all user's groups
$groups_ids = $this->getUserGroups($user_id);
- // build array of ACL variants
- if($object_class == '*') {
- $domain = [
- [ ['class_name', '=', '*'], ['user_id', '=', $user_id], ['object_id', 'is', null] ]
- ];
+ // retrieve all derived wildcard with class namespace
+ $wildcards = [];
+ $parts = explode('\\', $wildcard);
+ for($i = 0, $n = count($parts); $i < $n; ++$i) {
+ $level = array_slice($parts, 0, $i);
+ $level_wildcard = implode('\\', $level);
+ if($i < $n) {
+ $level_wildcard .= (strlen($level_wildcard))?'\*':'*';
+ }
+ $wildcards[] = $level_wildcard;
+ }
+
+ // build domain
+ $domain = [];
+ foreach($wildcards as $card) {
+ $domain[] = [ ['class_name', '=', $card], ['user_id', '=', $user_id], ['object_id', 'is', null] ];
if(count($groups_ids)) {
- $domain[] = [ ['class_name', '=', '*'], ['group_id', 'in', $groups_ids], ['object_id', 'is', null] ];
+ $domain[] = [ ['class_name', '=', $card], ['group_id', 'in', $groups_ids], ['object_id', 'is', null] ];
+ }
+ }
+
+ $acl_ids = $orm->search('core\Permission', $domain);
+ if($acl_ids > 0 && count($acl_ids)) {
+ $acls = $orm->read('core\Permission', $acl_ids, ['rights']);
+ foreach($acls as $acl_id => $acl) {
+ $result |= $acl['rights'];
}
}
- else {
- $domain = [];
- // add parent classes to the domain (when a right is granted on a class, it is also granted on children classes)
- $classes = $orm->getObjectParentsClasses($object_class);
- $classes[] = $object_class;
-
- foreach($classes as $class) {
- // extract package parts from class name (for wildcard notation)
- $package_parts = explode('\\', $class);
-
- // add disjunctions
- for($i = 0, $n = count($package_parts), $has_groups = count($groups_ids); $i <= $n; ++$i) {
- $level = array_slice($package_parts, 0, $i);
- $level_wildcard = implode('\\', $level);
- if($i < $n) {
- $level_wildcard .= (strlen($level_wildcard))?'\*':'*';
- }
- $domain[] = [ ['class_name', '=', $level_wildcard], ['user_id', '=', $user_id], ['object_id', 'is', null] ];
+ return $result;
+ }
- if($has_groups) {
- $domain[] = [ ['class_name', '=', $level_wildcard], ['group_id', 'in', $groups_ids], ['object_id', 'is', null] ];
- }
- }
+ /**
+ * Retrieve the minimal rights set on the given collection of objects.
+ * In case no specific right is defined for the collection 0 is returned (default_rights is not considered).
+ *
+ * @param int $user_id Identifier of targeted user.
+ * @param string $object_class Name of the class for which the ACL are requested.
+ *
+ */
+ private function getUserRightsOnObjects($user_id, $object_class, $objects_ids) {
+ $result = 0;
+
+ // `object_class` should be an explicit class, if not provide default rights
+ if(!class_exists($object_class)) {
+ return $result;
+ }
+
+ $orm = $this->container->get('orm');
+
+ // get all user's groups
+ $groups_ids = $this->getUserGroups($user_id);
+
+ // retrieve applicable parent classes (object from classes sharing same objects ids)
+ $classes = [$object_class];
+ $parent_classes = $orm->getObjectParentsClasses($object_class);
+
+ $table_name = $orm->getObjectTableName($object_class);
+ foreach($parent_classes as $class) {
+ if($orm->getObjectTableName($class) == $table_name) {
+ $classes[] = $class;
+ }
+ }
+
+ // build domain
+ $domain = [];
+
+ foreach($classes as $class) {
+ $domain[] = [ ['class_name', '=', $class], ['user_id', '=', $user_id], ['object_id', 'in', $objects_ids] ];
+ if(count($groups_ids)) {
+ $domain[] = [ ['class_name', '=', $class], ['group_id', 'in', $groups_ids], ['object_id', 'in', $objects_ids] ];
}
}
- // fetch all ACLs variants
$acl_ids = $orm->search('core\Permission', $domain);
- if(count($acl_ids)) {
- // get the user ACL
- $result = $orm->read('core\Permission', $acl_ids, ['rights']);
+ if($acl_ids > 0 && count($acl_ids)) {
+ $acls = $orm->read('core\Permission', $acl_ids, ['object_id', 'rights']);
+ $map_objects_rights = [];
+ // step-1 - map acl by object id (on a same object, rights are added)
+ foreach($acls as $acl_id => $acl) {
+ if(!isset($map_objects_rights[$acl['object_id']])) {
+ $map_objects_rights[$acl['object_id']] = 0;
+ }
+ $map_objects_rights[$acl['object_id']] |= $acl['rights'];
+ }
+ // step-2 - between objects, rights are subtracted (result must be the minimal right granted)
+ $resulting_rights = 0;
+ foreach($map_objects_rights as $object_id => $rights) {
+ if($rights == 0) {
+ $resulting_rights = 0;
+ break;
+ }
+ elseif($resulting_rights == 0) {
+ $resulting_rights = $rights;
+ }
+ else {
+ $resulting_rights &= $rights;
+ }
+ }
+ $result |= $resulting_rights;
}
return $result;
}
-
/**
* Arbitrary change the rights granted to a user or a group.
*
- * @param $identity string 'user' or 'group'
- * @param $operator string '+' or '-'
- * @param $rights integer binary mask of the rights to apply
- * @param $identity_id integer identifier of targeted user or group
- * @param $object_class string name of the class on which rights apply to : wildcards are allowed (ex. '*' or 'core\*')
+ * @param string $identity Type of identity on which the right must be changed: 'user' or 'group'.
+ * @param string $operator Operator to apply: '+' or '-'.
+ * @param integer $rights Bit mask of the rights to apply.
+ * @param integer $identity_id Identifier of targeted user or group.
+ * @param string $object_class Name of the class on which rights apply to : wildcards are allowed (ex. '*' or 'core\*').
*
*/
private function changeRights($identity, $operator, $rights, $identity_id, $object_class) {
@@ -260,7 +348,7 @@ private function changeRights($identity, $operator, $rights, $identity_id, $obje
}
else {
// update ACL with new permissions
- $orm->write('core\Permission', $acl_id, ['rights' => $acl['rights']]);
+ $orm->update('core\Permission', $acl_id, ['rights' => $acl['rights']]);
}
}
$rights = $acl['rights'];
@@ -320,7 +408,7 @@ public function revoke($operation, $object_class='*', $object_fields=[], $object
/**
* Add current user to a list of groups.
*
- * @param $groups_ids array List of groups identifiers the current user must be added to.
+ * @param int[] $groups_ids List of groups identifiers the current user must be added to.
*/
public function addGroups($groups_ids, $user_id=null) {
$groups_ids = (array) $groups_ids;
@@ -328,14 +416,17 @@ public function addGroups($groups_ids, $user_id=null) {
$auth = $this->container->get('auth');
$user_id = $auth->userId();
}
+ if(isset($this->permissionsTable[$user_id])) {
+ unset($this->permissionsTable[$user_id]);
+ }
$orm = $this->container->get('orm');
- return $orm->write('core\Group', $groups_ids, ['users_ids' => ["+{$user_id}"] ]);
+ return $orm->update('core\Group', $groups_ids, ['users_ids' => ["+{$user_id}"] ]);
}
/**
* Alias of addGroups.
*
- * @param $group_id integer Identifier of the group the user must be added to.
+ * @param int $group_id Identifier of the group the user must be added to.
*/
public function addGroup($group_id, $user_id=null) {
return $this->addGroups($group_id, $user_id);
@@ -344,7 +435,7 @@ public function addGroup($group_id, $user_id=null) {
/**
* Check if a user is member of a given group. I no user is provided, defaults to current user.
*
- * @param $group integer|string The group name or identifier.
+ * @param integer|string $group The group name or identifier.
*/
public function hasGroup($group, $user_id=null) {
$orm = $this->container->get('orm');
@@ -367,7 +458,7 @@ public function hasGroup($group, $user_id=null) {
/**
* Check if a given user (retrieved using Auth service) is explicitly granted the requested rights to perform a given operation.
*
- * @param integer $operation Binary mask of the operations that are checked (can be built using constants : QN_R_CREATE, QN_R_READ, QN_R_DELETE, QN_R_WRITE, QN_R_MANAGE).
+ * @param integer $operation Bit mask of the operations that are checked (can be built using constants : EQ_R_CREATE, EQ_R_READ, EQ_R_DELETE, EQ_R_WRITE, EQ_R_MANAGE).
* @param string $object_class Class selector indicating on which classes the check must be performed.
* @param int[] $objects_ids (optional) List of objects identifiers (relating to $object_class) against which the check must be performed.
*/
@@ -375,7 +466,7 @@ public function hasRight($user_id, $operation, $object_class='*', $objects_ids=[
// force cast ids to array (passing a single id is accepted)
$objects_ids = (array) $objects_ids;
// permission query is for class and/or fields only (no specific objects)
- $user_rights = $this->getUserRights($user_id, $object_class, $objects_ids);
+ $user_rights = $this->getUserRights($user_id, $object_class, $objects_ids, $operation);
// if all bits of operation are granted, then user has requested rights
return (($user_rights & $operation) == $operation);
}
@@ -384,9 +475,9 @@ public function hasRight($user_id, $operation, $object_class='*', $objects_ids=[
* Check if current user (retrieved using Auth service) has rights to perform a given operation.
*
* This method is called by the Collection service, when performing CRUD.
- * #todo - deprecate $object_fields
+ * #todo #confirm - deprecate $object_fields (how individual can...() checks are made on fields ?)
*
- * @param integer $operation Identifier of the operation(s) that is/are checked (binary mask made of constants : QN_R_CREATE, QN_R_READ, QN_R_DELETE, QN_R_WRITE, QN_R_MANAGE).
+ * @param integer $operation Identifier of the operation(s) that is/are checked (bit mask made of constants : EQ_R_CREATE, EQ_R_READ, EQ_R_DELETE, EQ_R_WRITE, EQ_R_MANAGE).
* @param string $object_class Class selector indicating on which classes the check must be performed.
* @param string[] $object_fields (optional) List of fields name on which the operation must be granted.
* @param int[] $object_ids (optional) List of objects identifiers (relating to $object_class) against which the check must be performed.
@@ -403,6 +494,51 @@ public function isAllowed($operation, $object_class='*', $object_fields=[], $obj
return $has_right;
}
+ /**
+ * Check if a given user is granted a role on a collection of objects.
+ *
+ * @var integer $user_id The identifier of the user for which the test is requested.
+ * @var string $role The role for which assignment is being tested.
+ * @param string $object_class Class on which the check must be performed.
+ * @param int[] $object_ids List of objects identifiers (relating to $object_class) against which the check must be performed.
+ */
+ public function hasRole($user_id, $role, $object_class, $objects_ids=[]) {
+ // associative array with keys holding objects ids for which role is granted
+ $result = [];
+
+ /** @var \equal\orm\ObjectManager */
+ $orm = $this->container->get('orm');
+
+ // build a list of all roles that cover the given role (see implied_by)
+ $def_roles = $object_class::getRoles();
+ $map_roles = [];
+
+ if(isset($def_roles[$role])) {
+ $map_roles[$role] = true;
+ $desc = $def_roles[$role];
+ while(isset($desc['implied_by'])) {
+ foreach((array) $desc['implied_by'] as $r) {
+ $map_roles[$r] = true;
+ }
+ $desc = $desc['implied_by'];
+ }
+ }
+
+ $related_roles = array_keys($map_roles);
+ if(count($related_roles)) {
+ foreach($objects_ids as $object_id) {
+ // retrieve all assignments objects implying one or more roles of the list, given to user on given object_class, object_id
+ $user_roles_ids = $orm->search('core\Assignment', [['object_id', '=', $object_id], ['object_class', '=', $object_class], ['user_id', '=', $user_id], ['role', 'in', $related_roles]]);
+ if($user_roles_ids > 0 && count($user_roles_ids)) {
+ // map results on object_id
+ $result[$object_id] = true;
+ }
+ }
+ }
+
+ return (count(array_keys($result)) == count($objects_ids));
+ }
+
/**
* Check if a Collection is compliant with a given policy for a specific User.
* This relates to the optional `getPolicies()` method that can be defined on Model classes.
@@ -465,106 +601,4 @@ public function canPerform($user_id, $action, $object_class, $object_ids) {
return $result;
}
- /**
- * @deprecated
- */
- /*
- public function rights($user_id, $object_class, $object_fields=[]) {
- // non-root users can only fetch their own rights
- $auth = $this->container->get('auth');
- $uid = $auth->userId();
- if($uid != QN_ROOT_USER_ID) {
- $user_id = $uid;
- }
- return $this->getUserRights($user_id, $object_class);
- }
- */
-
- /**
- * @deprecated
- *
- */
- /*
- public function groups($user_id) {
- // non-root user can only fetch their own groups
- $auth = $this->container->get('auth');
- $uid = $auth->userId();
- if($uid != QN_ROOT_USER_ID) {
- $user_id = $uid;
- }
- return $this->getUserGroups($user_id);
- }
- */
-
- /**
- * Filter a list of objects and return only ids of objects on which current user has permission for given operation.
- * This method is called by the Collection service, when performing Search
- * @deprecated
- */
- // public function filter($operation, $object_class, $object_fields, $object_ids) {
-
- // // grant all rights when using CLI
- // if(php_sapi_name() === 'cli') return $object_ids;
-
- // /** @var ObjectManager */
- // $orm = $this->container->get('orm');
-
- // // retrieve current user identifier
- // $auth = $this->container->get('auth');
- // $user_id = $auth->userId();
-
- // // build final user rights
- // $user_rights = $this->getUserRights($user_id, $object_class, $object_ids);
-
- // // retrieve root class of filtered entity
- // $root_class = ObjectManager::getObjectRootClass($object_class);
-
- // if($root_class == 'core\Group') {
- // // members of a group have READ rights on it
- // $groups_ids = $this->getUserGroups($user_id);
- // if(count(array_diff($object_ids, $groups_ids)) <= 0) {
- // $user_rights |= QN_R_READ;
- // }
- // }
- // elseif($root_class == 'core\User') {
- // if(count($object_ids) == 1 && $user_id == $object_ids[0]) {
- // // user always has READ rights on its own object
- // $user_rights |= QN_R_READ;
- // // user is granted to update some fields on its own object
- // $writeable_fields = ['password', 'firstname', 'lastname', 'language', 'locale'];
- // // if, after removing special fields, there are only fields that user can update, then we grant the WRITE right
- // if(count(array_diff($object_fields, array_merge(array_keys(Model::getSpecialColumns()), $writeable_fields))) == 0) {
- // $user_rights |= QN_R_WRITE;
- // }
- // }
- // }
- // elseif($operation == QN_R_READ) {
- // // this is a special case of a generic feature (we should add this in the init data)
- // // if all fields 'creator' of targeted objects are equal to $user_id, then add R_READ to user_rights
- // $objects = $orm->read($object_class, $object_ids, ['creator']);
- // $user_ids = [];
- // foreach($objects as $oid => $odata) {
- // $user_ids[$odata['creator']] = true;
- // }
- // // user always has READ right on objects he created
- // if(count($user_ids) == 1 && array_keys($user_ids)[0] == $user_id) {
- // $user_rights |= QN_R_READ;
- // }
- // }
-
-
- // /*
- // loop on objects_ids, for each object
-
- // 1) fetch regular ACL
- // 2) fetch ACL with a domain
- // either user has operation granted with one of its ACL (or one of its groups)
- // or user is granted operation on specific objects (matching ACL domain)
-
- // validate id as soon as an ACL condition is met
- // */
-
- // return ((bool)($user_rights & $operation))?$object_ids:[];
- // }
-
-}
\ No newline at end of file
+}
diff --git a/lib/equal/data/adapt/DataAdapterProviderSql.class.php b/lib/equal/data/adapt/DataAdapterProviderSql.class.php
index 1d843937d..4227d25cb 100644
--- a/lib/equal/data/adapt/DataAdapterProviderSql.class.php
+++ b/lib/equal/data/adapt/DataAdapterProviderSql.class.php
@@ -10,7 +10,8 @@ class DataAdapterProviderSql implements AdapterProvider {
const CONFIG = [
'MYSQL' => 'equal\data\adapt\DataAdapterProviderSqlMySql',
'MARIADB' => 'equal\data\adapt\DataAdapterProviderSqlMySql',
- 'SQLSRV' => 'equal\data\adapt\DataAdapterProviderSqlSqlSrv'
+ 'SQLSRV' => 'equal\data\adapt\DataAdapterProviderSqlSqlSrv',
+ 'SQLITE' => 'equal\data\adapt\DataAdapterProviderSqlSqlite'
];
/**
diff --git a/lib/equal/data/adapt/DataAdapterProviderSqlSqlite.class.php b/lib/equal/data/adapt/DataAdapterProviderSqlSqlite.class.php
new file mode 100644
index 000000000..2c7ac2a97
--- /dev/null
+++ b/lib/equal/data/adapt/DataAdapterProviderSqlSqlite.class.php
@@ -0,0 +1,93 @@
+
+ Some Rights Reserved, Cedric Francoys, 2010-2024
+ Licensed under GNU LGPL 3 license
+*/
+namespace equal\data\adapt;
+
+use equal\orm\UsageFactory;
+
+class DataAdapterProviderSqlSqlite implements AdapterProvider {
+ const CONFIG = [
+ // keys match the name of the supported UsageTypes
+ 'number' => [
+ 'default' => 'equal\data\adapt\adapters\sql\DataAdapterSqlInteger',
+ 'boolean' => 'equal\data\adapt\adapters\sql\DataAdapterSqlBoolean',
+ 'natural' => 'equal\data\adapt\adapters\sql\DataAdapterSqlInteger',
+ 'integer' => 'equal\data\adapt\adapters\sql\DataAdapterSqlInteger',
+ 'real' => 'equal\data\adapt\adapters\sql\DataAdapterSqlReal'
+ ],
+ 'amount' => [
+ 'default' => 'equal\data\adapt\adapters\sql\DataAdapterSqlReal'
+ ],
+ 'text' => [
+ 'default' => 'equal\data\adapt\adapters\sql\DataAdapterSqlText'
+ ],
+ 'time' => [
+ 'default' => 'equal\data\adapt\adapters\sql\DataAdapterSqlTime'
+ ],
+ 'datetime' => [
+ 'default' => 'equal\data\adapt\adapters\sql\DataAdapterSqlDateTime'
+ ],
+ 'date' => [
+ 'default' => 'equal\data\adapt\adapters\sql\DataAdapterSqlDate',
+ 'plain' => 'equal\data\adapt\adapters\sql\DataAdapterSqlDate',
+ 'datetime' => 'equal\data\adapt\adapters\sql\DataAdapterSqlDateTime',
+ 'time' => 'equal\data\adapt\adapters\sql\DataAdapterSqlDateTime',
+ 'year' => 'equal\data\adapt\adapters\sql\DataAdapterSqlDateYear',
+ 'month' => 'equal\data\adapt\adapters\sql\DataAdapterSqlDateMonth'
+ ],
+ 'image' => [
+ 'default' => 'equal\data\adapt\adapters\sql\DataAdapterSqlBinary'
+ ],
+ 'binary' => [
+ 'default' => 'equal\data\adapt\adapters\sql\DataAdapterSqlBinary'
+ ],
+ 'array' => [
+ 'default' => 'equal\data\adapt\adapters\sql\DataAdapterSqlArray'
+ ]
+ ];
+
+ /**
+ * Provides a DataAdapter instance, according to the given content type.
+ * This method supports the Content-Type syntax (type/subtype;parameter=value) along with major format aliases (JSON, SQL, TXT).
+ *
+ * @param string $content_type The content type or the type-alias for which the DataAdapter has to be returned.
+ * We try as much as possible to use standard Content-Types as defined by RFC7231 and listed by IANA (https://www.iana.org/assignments/media-types/media-types.xhtml).
+ * When adaptation implies more specific distinction, we use subtype tree for distinguishing adapters.
+ *
+ * @return DataAdapter
+ *
+ * @example
+ * application/json
+ * application/sql
+ * application/xml
+ * text/plain
+ * application/sql.t-sql
+ *
+ */
+ public function get(string $content_type) {
+ /** @var \equal\orm\usages\Usage */
+ $usage = UsageFactory::create($content_type);
+ $type = $usage->getType();
+ $subtype = $usage->getSubtype();
+ // default adapter (identity - no conversion)
+ $adapter = 'equal\data\adapt\adapters\DataAdapterDefault';
+ if(isset(self::CONFIG[$type])) {
+ if(isset(self::CONFIG[$type][$subtype])) {
+ $adapter = self::CONFIG[$type][$subtype];
+ }
+ elseif(isset(self::CONFIG[$type]['default'])) {
+ $adapter = self::CONFIG[$type]['default'];
+ }
+ else {
+ // #todo - issue a log entry (missing default)
+ }
+ }
+ else {
+ // #todo - issue a log entry
+ }
+ return new $adapter;
+ }
+}
diff --git a/lib/equal/data/adapt/adapters/sql/DataAdapterSqlText.class.php b/lib/equal/data/adapt/adapters/sql/DataAdapterSqlText.class.php
index 89772ae81..646854511 100644
--- a/lib/equal/data/adapt/adapters/sql/DataAdapterSqlText.class.php
+++ b/lib/equal/data/adapt/adapters/sql/DataAdapterSqlText.class.php
@@ -16,7 +16,7 @@ public function getType() {
/**
* Handles the conversion to the PHP type equivalent.
- * Adapts the input value from JSON type to PHP type (JSON -> PHP).
+ * Adapts the input value from JSON type to PHP type (SQL -> PHP).
*
* @param mixed $value Value to be adapted.
* @param string|Usage $usage The usage descriptor the adaptation is requested for.
@@ -33,7 +33,7 @@ public function adaptIn($value, $usage, $locale='en') {
/**
* Handles the conversion to the type targeted by the DataAdapter.
- * Adapts the input value from PHP type to JSON type (PHP -> JSON).
+ * Adapts the input value from PHP type to JSON type (PHP -> SQL).
*
* @param mixed $value Value to be adapted.
* @param string|Usage $usage The usage descriptor the adaptation is requested for.
diff --git a/lib/equal/data/adapt/adapters/txt/DataAdapterTxtInteger.class.php b/lib/equal/data/adapt/adapters/txt/DataAdapterTxtInteger.class.php
index 31307d97d..7b0e63804 100644
--- a/lib/equal/data/adapt/adapters/txt/DataAdapterTxtInteger.class.php
+++ b/lib/equal/data/adapt/adapters/txt/DataAdapterTxtInteger.class.php
@@ -7,6 +7,7 @@
namespace equal\data\adapt\adapters\txt;
use equal\data\adapt\DataAdapter;
+use equal\locale\Locale;
class DataAdapterTxtInteger implements DataAdapter {
diff --git a/lib/equal/data/adapt/adapters/txt/DataAdapterTxtTime.class.php b/lib/equal/data/adapt/adapters/txt/DataAdapterTxtTime.class.php
index cbda978c9..a467abe2d 100644
--- a/lib/equal/data/adapt/adapters/txt/DataAdapterTxtTime.class.php
+++ b/lib/equal/data/adapt/adapters/txt/DataAdapterTxtTime.class.php
@@ -8,6 +8,7 @@
use equal\data\adapt\DataAdapter;
use equal\data\DataFormatter;
+use equal\locale\Locale;
class DataAdapterTxtTime implements DataAdapter {
diff --git a/lib/equal/db/DBConnection.class.php b/lib/equal/db/DBConnection.class.php
index 67fa04453..04a7de2b5 100644
--- a/lib/equal/db/DBConnection.class.php
+++ b/lib/equal/db/DBConnection.class.php
@@ -43,6 +43,17 @@ protected function __construct() {
constant('DB_COLLATION')
);
break;
+ case 'SQLITE' :
+ $this->dbConnection = new DBManipulatorSQLite(
+ constant('DB_HOST'),
+ constant('DB_PORT'),
+ constant('DB_NAME'),
+ constant('DB_USER'),
+ constant('DB_PASSWORD'),
+ constant('DB_CHARSET'),
+ constant('DB_COLLATION')
+ );
+ break;
case 'POSTGRESQL' :
// #todo
break;
@@ -125,4 +136,4 @@ public function __call($name, $arguments) {
return call_user_func_array([$this->dbConnection, $name], $arguments);
}
-}
\ No newline at end of file
+}
diff --git a/lib/equal/db/DBManipulator.class.php b/lib/equal/db/DBManipulator.class.php
index fae5b8975..6c541c069 100644
--- a/lib/equal/db/DBManipulator.class.php
+++ b/lib/equal/db/DBManipulator.class.php
@@ -168,7 +168,7 @@ public function getSqlType($type) {
* @return boolean false if no connection can be made, true otherwise
*
*/
- public final function canConnect() {
+ public function canConnect() {
if($fp = fsockopen($this->host, $this->port, $errno, $errstr, 1)) {
fclose($fp);
return true;
diff --git a/lib/equal/db/DBManipulatorMySQL.class.php b/lib/equal/db/DBManipulatorMySQL.class.php
index 84443d7eb..1546ccd93 100644
--- a/lib/equal/db/DBManipulatorMySQL.class.php
+++ b/lib/equal/db/DBManipulatorMySQL.class.php
@@ -39,7 +39,8 @@ public function select($db_name) {
}
/**
- * Open the DBMS connection
+ * Open the DBMS connection.
+ * This method is meant to assign a value to `$this->dbms_handler`.
*
* @param boolean $auto_select Automatically connect to provided database (otherwise the connection is established only wity the DBMS server)
* @return integer The status of the connect function call.
@@ -226,7 +227,7 @@ public function getQueryAddRecords($table, $fields, $values) {
}
$vals = rtrim($vals, ',');
if(strlen($cols) > 0 && strlen($vals) > 0) {
- // #memo - we ignore duplicate enties, if any
+ // #memo - we ignore duplicate entries, if any
$sql = "INSERT IGNORE INTO `$table` ($cols) VALUES $vals;";
}
return $sql;
diff --git a/lib/equal/db/DBManipulatorSQLite.class.php b/lib/equal/db/DBManipulatorSQLite.class.php
new file mode 100644
index 000000000..8a0b32e44
--- /dev/null
+++ b/lib/equal/db/DBManipulatorSQLite.class.php
@@ -0,0 +1,558 @@
+
+ Some Rights Reserved, Cedric Francoys, 2010-2021
+ Licensed under GNU LGPL 3 license
+*/
+namespace equal\db;
+
+/**
+ * DBManipulator implementation for MySQL server.
+ *
+ */
+
+class DBManipulatorSQLite extends DBManipulator {
+
+
+ public static $types_associations = [
+ 'boolean' => 'INTEGER',
+ 'integer' => 'INTEGER',
+ 'float' => 'REAL',
+ 'string' => 'TEXT',
+ 'text' => 'TEXT',
+ 'date' => 'TEXT',
+ 'time' => 'TEXT',
+ 'datetime' => 'TEXT',
+ 'binary' => 'BLOB',
+ 'many2one' => 'INTEGER'
+ ];
+
+ public function getSqlType($type) {
+ if(isset(self::$types_associations[$type])) {
+ return self::$types_associations[$type];
+ }
+ return '';
+ }
+
+ public function select($db_name) {
+ return $this->dbms_handler;
+ }
+
+ public function canConnect() {
+ // by convention the DB file is the given DB_NAME with `.db` suffix
+ $db_file = QN_BASEDIR.'/bin/'.$this->db_name.'.db';
+
+ if(file_exists($db_file)) {
+ return is_writable($db_file);
+ }
+
+ return is_writable(QN_BASEDIR.'/bin/');
+ }
+
+ /**
+ * Open the DBMS connection.
+ * This method is meant to assign a value to `$this->dbms_handler`.
+ *
+ * @param boolean $auto_select Automatically connect to provided database (otherwise the connection is established only wity the DBMS server)
+ * @return integer The status of the connect function call.
+ * @access public
+ */
+ public function connect($auto_select=true) {
+
+ if( !class_exists( 'SQLite3' ) ) {
+ throw new \Exception('missing_dependency', QN_ERROR_INVALID_CONFIG);
+ }
+
+ // by convention the DB file is the given DB_NAME with `.db` suffix
+ $db_file = QN_BASEDIR.'/bin/'.$this->db_name.'.db';
+
+ $this->dbms_handler = new \SQLite3($db_file, SQLITE3_OPEN_READWRITE | SQLITE3_OPEN_CREATE, $this->password);
+
+ if(!file_exists($db_file)) {
+ return false;
+ }
+
+ return $this;
+ }
+
+
+ /**
+ * Close the DBMS connection
+ *
+ * @return integer Status of the close function call
+ * @access public
+ */
+ public function disconnect() {
+ if(isset($this->dbms_handler)) {
+ $this->dbms_handler->close();
+ $this->dbms_handler = null;
+ }
+ return true;
+ }
+
+ public function createDatabase($db_name) {
+ // SQLite only supports 1 DB per file
+
+ $query = "CREATE DATABASE IF NOT EXISTS $db_name";
+
+ // SQLite supports only the UTF-8 character encoding
+ /*
+
+ if($this->charset) {
+ $query .= " CHARACTER SET ".$this->charset;
+ }
+ if($this->collation) {
+ $query .= " COLLATE ".$this->collation.';';
+ }
+ */
+ $query .= ";";
+ // $this->sendQuery($query);
+ }
+
+ public function getTables() {
+ $tables = [];
+ $query = "SHOW TABLES;";
+ $res = $this->sendQuery($query);
+ while ($row = $this->fetchRow($res)) {
+ $tables[] = $row[0];
+ }
+ return $tables;
+ }
+
+ public function getTableSchema($table_name) {
+ $schema = [];
+ // expected properties: Field, Type, Collation, Null, Default (not used: Key, Extra, Privileges, Comment)
+ $query = "SHOW FULL COLUMNS FROM `{$table_name}`;";
+ $res = $this->sendQuery($query);
+ while($row = $this->fetchArray($res)) {
+ $field = $row['Field'];
+ $schema[$field] = [
+ 'type' => substr($row['Type'], 0, strpos($row['Type'].'(', '(')),
+ 'collation' => $row['Collation'],
+ 'nullable' => $row['Null'],
+ 'default' => $row['Default']
+ ];
+ }
+ return $schema;
+ }
+
+ public function getTableColumns($table_name) {
+ $query = "PRAGMA table_info($table_name);";
+ $res = $this->sendQuery($query);
+ $columns = [];
+ while ($row = $this->fetchArray($res)) {
+ $columns[] = $row['name'];
+ }
+ return $columns;
+ }
+
+ public function getTableConstraints($table_name) {
+ $query = "PRAGMA table_info($table_name);";
+ $res = $this->sendQuery($query);
+ $constraints = [];
+ while ($row = $this->fetchArray($res)) {
+ if($row['pk']) {
+ $constraints[] = $row['name'];
+ }
+ }
+ return $constraints;
+ }
+
+ public function getQueryCreateTable($table_name) {
+ // #memo - autoincrement keyword make the autoincrement break !
+ $query = "CREATE TABLE IF NOT EXISTS `{$table_name}` (id INTEGER PRIMARY KEY)";
+ // SQLite supports only the UTF-8 character encoding
+ /*
+ if($this->charset) {
+ $query .= " DEFAULT CHARSET=".$this->charset;
+ }
+ if($this->collation) {
+ $query .= " COLLATE=".$this->collation;
+ }
+ */
+ // #memo - we must add at least one column, so as a convention we add the id column
+ 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']}";
+ // set query according to primary key property
+ if(isset($def['auto_increment']) && $def['auto_increment']) {
+ $sql .= ' AUTO_INCREMENT';
+ }
+ // #memo - in SQLITE all fields non-nullable fields must have a default value
+ if(isset($def['null']) && !$def['null']) {
+ $sql .= ' NOT NULL DEFAULT 1';
+ }
+ // #memo - default is supported by ORM, not DBMS
+ if(!isset($def['null']) || $def['null']) {
+ // unless specified otherwise, all columns can be null (even if having a default value)
+ $sql .= ' DEFAULT NULL';
+ }
+
+ $sql .= ';';
+
+ if(isset($def['primary']) && $def['primary']) {
+ $sql .= "ALTER TABLE `{$table_name}` ADD PRIMARY KEY (`{$column_name}`);";
+ }
+ return $sql;
+ }
+
+ public function getQueryAddConstraint($table_name, $columns) {
+ return "CREATE UNIQUE INDEX ".implode('_', $columns)." ON {$table_name}(".implode(',', $columns).");";
+ }
+
+ public function getQueryAddRecords($table, $fields, $values) {
+ $sql = '';
+ if (!is_array($fields) || !is_array($values)) {
+ throw new \Exception(__METHOD__.' : at least one parameter is missing', QN_ERROR_SQL);
+ }
+ $cols = '';
+ $vals = '';
+ foreach ($fields as $field) {
+ $cols .= "`$field`,";
+ }
+ $cols = rtrim($cols, ',');
+ foreach ($values as $val_array) {
+ $vals .= '(';
+ foreach($val_array as $val) {
+ $vals .= $this->escapeString($val).',';
+ }
+ $vals = rtrim($vals, ',').'),';
+ }
+ $vals = rtrim($vals, ',');
+ if(strlen($cols) > 0 && strlen($vals) > 0) {
+ // #memo - we ignore duplicate entries, if any
+ $sql = "INSERT OR IGNORE INTO `$table` ($cols) VALUES $vals;";
+ }
+ return $sql;
+ }
+
+ /**
+ * Sends a SQL query.
+ *
+ * @param string The query to send to the DBMS.
+ *
+ * @return resource Returns a resource identifier or -1 if the query was not executed correctly.
+ */
+ public function sendQuery($query) {
+ trigger_error("SQL::$query", QN_REPORT_DEBUG);
+
+ if(!strlen($query)) {
+ trigger_error("SQL::ignoring empty query", QN_REPORT_DEBUG);
+ return;
+ }
+
+ if(($result = $this->dbms_handler->query($query)) === false) {
+ throw new \Exception(__METHOD__.' : query failure. '.$this->dbms_handler->lastErrorMsg().'. For query: "'.$query.'"', QN_ERROR_SQL);
+ }
+ // everything went well: perform additional operations (replication & info about query result)
+ else {
+ // update $affected_rows, $last_query, $last_id (depending on the performed operation)
+ $sql_operation = strtolower((explode(' ', $query, 2))[0]);
+ $this->setLastQuery($query);
+ if($sql_operation == 'select' || $sql_operation == 'show') {
+ $this->setAffectedRows($this->dbms_handler->changes());
+ }
+ else {
+ // for WRITE operations, relay query to members of the replica
+ if(in_array($sql_operation, ['insert', 'update', 'delete', 'drop', 'create'])) {
+ foreach($this->members as $member) {
+ $member->sendQuery($query);
+ }
+ }
+ if($sql_operation =='insert') {
+ $this->setLastId($this->dbms_handler->lastInsertRowID());
+ }
+ $this->setAffectedRows($this->dbms_handler->changes());
+ }
+ }
+ return $result;
+ }
+
+ /**
+ * @param SQLite3Result $result
+ */
+ public static function fetchRow($result) {
+ return $result->fetchArray(SQLITE3_NUM);
+ }
+
+ /**
+ * @param SQLite3Result $result
+ */
+ public static function fetchArray($result) {
+ return $result->fetchArray(SQLITE3_ASSOC);
+ }
+
+ /**
+ * Escapes a string containing the name of an object's field to match the SQL notation : `table`.`field` or `field`
+ *
+ * @param string $field_name
+ * @return string
+ */
+ private static function escapeFieldName($field_name) {
+ $parts = explode('.', str_replace('`', '', $field_name));
+ return (count($parts) > 1)?"`{$parts[0]}`.`{$parts[1]}`":"`{$parts[0]}`";
+ }
+
+ /**
+ * Escapes a string for safe SQL insertion
+ *
+ * @param string $value
+ * @return string
+ */
+ private function escapeString($value) {
+ $result = '';
+ if(gettype($value) == 'string' && strlen($value) == 0) {
+ $result = "''";
+ }
+ elseif(in_array(gettype($value), ['integer', 'double'])) {
+ $result = $value;
+ }
+ elseif(gettype($value) == 'boolean') {
+ $result = ($value)?'1':'0';
+ }
+ elseif(is_null($value)) {
+ $result = 'NULL';
+ }
+ else {
+ $value = (string) $value;
+ // value is a field name
+ if(strlen($value) && substr($value, 0, 1) == '`') {
+ $result = self::escapeFieldName($value);
+ }
+ // value represents NULL SQL value
+ elseif($value == 'null' || $value == 'NULL') {
+ $result = 'NULL';
+ }
+ // value is any other kind of string
+ else {
+ if(substr($value, 0, 3) == "h0x") {
+ // hexadecimal string that must be stored as a binary value
+ $result = "X'".substr($value, 3)."'";
+ }
+ else {
+ // regular string that must be escaped
+ $result = "'".$this->dbms_handler->escapeString($value)."'";
+ }
+ }
+ }
+ return $result;
+ }
+
+ /**
+ * Gets the SQL WHERE clause
+ *
+ * @param string $id_field
+ * @param array $ids
+ * @param array $conditions
+ *
+ * array( array( array(operand, operator, operand)[, array(operand, operator, operand) [, ...]]) [, array( array(operand, operator, operand)[, array(operand, operator, operand) [, ...]])])
+ * array of several series of clauses joined by logical ANDs themselves joined by logical ORs : disjunctions of conjunctions
+ * i.e.: (clause[, AND clause [, AND ...]) OR (clause[, AND clause [, AND ...])
+ */
+ private function getConditionClause($id_field, $ids, $conditions) {
+ $sql = '';
+ if(empty($conditions)) {
+ $conditions = [[[]]];
+ }
+ for($j = 0, $max_j = count($conditions); $j < $max_j; ++$j) {
+ if($j > 0 && strlen($sql) > 0) {
+ $sql .= ') OR (';
+ }
+ if(!empty($ids)) {
+ $conditions[$j][] = array($id_field, 'in', $ids);
+ }
+ for($i = 0, $max_i = count($conditions[$j]); $i < $max_i; ++$i) {
+ if($i > 0 && strlen($sql) > 0) {
+ $sql .= ' AND ';
+ }
+ $cond = $conditions[$j][$i];
+ if(!count($cond)) {
+ continue;
+ }
+ // adjust the field syntax (if necessary)
+ $cond[0] = self::escapeFieldName($cond[0]);
+ // operator 'in' having a single value as right operand
+ if((strcasecmp($cond[1], 'in') == 0 || strcasecmp($cond[1], 'not in') == 0) && !is_array($cond[2])) {
+ $cond[2] = (array) $cond[2];
+ }
+ // case-sensitive is the default behavior
+ if(strcasecmp($cond[1], 'like') == 0) {
+ $cond[1] = 'LIKE';
+ }
+ // #todo - should we use `COLLATE NOCASE` ?
+ if(strcasecmp($cond[1], 'ilike') == 0) {
+ // force sqlite to handle the field as a char (necessary for translations that are stored in a binary field)
+ $cond[0] = 'CAST('.$cond[0].' AS CHAR )';
+ $cond[1] = 'LIKE';
+ }
+ // format the value operand
+ if(is_array($cond[2])) {
+ $value = '('.implode(',', array_map( [$this, 'escapeString'], $cond[2] )).')';
+ }
+ else {
+ $value = $this->escapeString($cond[2]);
+ }
+ // concatenate query string with current condition
+ $sql .= $cond[0].' '.$cond[1].' '.$value;
+ }
+ }
+ if(strlen($sql) > 0) {
+ $sql = ' WHERE ('.$sql.')';
+ }
+ return $sql;
+ }
+
+ /**
+ * Get records from specified table, according to some conditions.
+ *
+ * @param array $tables name of involved tables
+ * @param array $fields list of requested fields
+ * @param array $ids ids to which the selection is limited
+ * @param array $conditions list of arrays (field, operand, value)
+ * @param string $id_field name of the id field ('id' by default)
+ * @param mixed $order string holding name of the order field or maps holding field names as keys and sorting as value
+ * @param integer $start
+ * @param integer $limit
+ *
+ * @return resource reference to query resource
+ */
+ public function getRecords($tables, $fields=NULL, $ids=NULL, $conditions=NULL, $id_field='id', $order=[], $start=0, $limit=0) {
+ // cast tables to an array (passing a single table is accepted)
+ if(!is_array($tables)) {
+ $tables = (array) $tables;
+ }
+ // in case fields is not null ans is not an array, cast it to an array (passing a single field is accepted)
+ if(isset($fields) && !is_array($fields)) {
+ $fields = (array) $fields;
+ }
+ // in case ids is not null ans is not an array, cast it to an array (passing a single id is accepted)
+ if(isset($ids) && !is_array($ids)) {
+ $ids = (array) $ids;
+ }
+
+ // test values and types
+ if(empty($tables)) {
+ throw new \Exception(__METHOD__." : unable to build sql query, parameter 'tables' array is empty.", QN_ERROR_SQL);
+ }
+ /* irrelevant
+ if(!empty($fields) && !is_array($fields)) throw new \Exception(__METHOD__." : unable to build sql query, parameter 'fields' is not an array.", QN_ERROR_SQL);
+ if(!empty($ids) && !is_array($ids)) throw new \Exception(__METHOD__." : unable to build sql query, parameter 'ids' is not an array.", QN_ERROR_SQL);
+ */
+ if(!empty($conditions) && !is_array($conditions)) {
+ throw new \Exception(__METHOD__." : unable to build sql query, parameter 'conditions' is not an array.", QN_ERROR_SQL);
+ }
+
+ // SELECT clause
+ // we could add the following directive for better performance (disabled to maximize code portability)
+ // $sql = 'SELECT SQL_CALC_FOUND_ROWS ';
+ $sql = 'SELECT DISTINCT ';
+ if(empty($fields)) {
+ $sql .= '*';
+ }
+ else {
+ $selection = [];
+ foreach($fields as $field) {
+ $selection[] = self::escapeFieldName($field);
+ }
+ $sql .= implode(',', $selection);
+ }
+
+ // FROM clause
+ $sql .= ' FROM ';
+ foreach($tables as $table_alias => $table_name) {
+ if(!is_numeric($table_alias)) {
+ $sql .= '`'.$table_name.'` as `'.$table_alias.'`, ';
+ }
+ else {
+ $sql .= '`'.$table_name.'`, ';
+ }
+ }
+ $sql = rtrim($sql, ' ,');
+
+ // WHERE clause
+ $sql .= $this->getConditionClause($id_field, $ids, $conditions);
+
+ // order clause
+ if(!empty($order)) {
+ $order_clause = [];
+ if(!is_array($order)) {
+ $order = [$order => 'ASC'];
+ }
+ foreach($order as $field => $sort) {
+ $order_clause[] = self::escapeFieldName($field).' '.$sort;
+ }
+ $sql .= ' ORDER BY '.implode(',', $order_clause);
+ }
+
+ // LIMIT clause
+ if($limit) {
+ $sql .= sprintf(" LIMIT %d, %d", $start, $limit);
+ }
+ return $this->sendQuery($sql);
+ }
+
+ 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 .= $this->getConditionClause($id_field, $ids, $conditions);
+
+ return $this->sendQuery($sql);
+ }
+
+ /**
+ * Inserts new records in specified table.
+ *
+ * @param string $table name of the table in which insert the records
+ * @param array $fields list of involved fields
+ * @param array $values array of arrays specifying the values related to each specified field
+ * @return resource reference to query resource
+ */
+ public function addRecords($table, $fields, $values) {
+ if (!is_array($fields) || !is_array($values)) {
+ throw new \Exception(__METHOD__.' : at least one parameter is missing', QN_ERROR_SQL);
+ }
+ $sql = $this->getQueryAddRecords($table, $fields, $values);
+ return $this->sendQuery($sql);
+ }
+
+ public function deleteRecords($table, $ids, $conditions=null, $id_field='id') {
+ // delete clause
+ $sql = 'DELETE FROM `'.$table.'`';
+ // where clause
+ $sql .= $this->getConditionClause($id_field, $ids, $conditions);
+ return $this->sendQuery($sql);
+ }
+
+}
diff --git a/lib/equal/db/DBManipulatorSqlSrv.class.php b/lib/equal/db/DBManipulatorSqlSrv.class.php
index d1f16f821..113d2c4f1 100644
--- a/lib/equal/db/DBManipulatorSqlSrv.class.php
+++ b/lib/equal/db/DBManipulatorSqlSrv.class.php
@@ -105,7 +105,8 @@ public function connect($auto_select=true) {
}
/**
- * Close the DBMS connection
+ * Close the DBMS connection.
+ * This method is meant to assign a value to `$this->dbms_handler`.
*
* @return integer Status of the close function call
* @access public
@@ -244,7 +245,7 @@ public function getQueryAddRecords($table, $fields, $values) {
$vals[] = '('.implode(',', $line).')';
}
if(count($fields) && count($vals)) {
- // #todo ignore duplicate enties, if any
+ // #todo ignore duplicate entries, if any
$sql = "INSERT INTO [$table] (".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;";
diff --git a/lib/equal/dispatch/Dispatcher.class.php b/lib/equal/dispatch/Dispatcher.class.php
index 5e6dd9321..defe6ccc0 100644
--- a/lib/equal/dispatch/Dispatcher.class.php
+++ b/lib/equal/dispatch/Dispatcher.class.php
@@ -45,27 +45,29 @@ public function dispatch($message_model, $object_class, $object_id, $severity='n
$message_models_ids = $orm->search('core\alert\MessageModel', ['name', '=', $message_model]);
- if($message_models_ids > 0 ){
+ if($message_models_ids > 0 && count($message_models_ids)){
$message_model_id = reset($message_models_ids);
-
- $values = [
- 'message_model_id' => $message_model_id,
- 'object_class' => $object_class,
- 'object_id' => $object_id,
- 'severity' => $severity,
- 'controller' => $controller,
- 'params' => json_encode($params),
- 'links' => json_encode($links),
- 'user_id' => $user_id,
- 'group_id' => $group_id
- ];
-
- if(!count($orm->validate('core\alert\Message', [], $values, true, true))) {
- $orm->create('core\alert\Message', $values);
- // if targeted object has an 'alert' field (computed as convention), reset it
- $orm->update($object_class, $object_id, ['alert' => null]);
+ // prevent creating duplicates
+ $messages_ids = $orm->search('core\alert\Message', [['message_model_id', '=', $message_model_id], ['object_class', '=', $object_class], ['object_id', '=', $object_id]]);
+ if(!count($messages_ids)) {
+ $values = [
+ 'message_model_id' => $message_model_id,
+ 'object_class' => $object_class,
+ 'object_id' => $object_id,
+ 'severity' => $severity,
+ 'controller' => $controller,
+ 'params' => json_encode($params),
+ 'links' => json_encode($links),
+ 'user_id' => $user_id,
+ 'group_id' => $group_id
+ ];
+
+ if(!count($orm->validate('core\alert\Message', [], $values, true, true))) {
+ $orm->create('core\alert\Message', $values);
+ // if targeted object has an 'alert' field (computed as convention), reset it
+ $orm->update($object_class, $object_id, ['alert' => null]);
+ }
}
-
}
else {
trigger_error("PHP::unknown message model", E_USER_WARNING);
@@ -77,25 +79,24 @@ public function dispatch($message_model, $object_class, $object_id, $severity='n
* This should be invoked following a user request for removing the message.
* The message is deleted and, if it relates to a controller, a call is made (which, in turn, can lead to the message being re-created).
*
- * @param string $id Identifier of the message to cancel.
+ * @param integer $id Identifier of the message to cancel (core\alert\Message).
*/
public function dismiss($id) {
/** @var \equal\orm\ObjectManager */
$orm = $this->container->get('orm');
$messages_ids = $orm->search('core\alert\Message', ['id', '=', $id]);
- if($messages_ids > 0) {
- $message_id = reset($messages_ids);
- $messages = $orm->read('core\alert\Message', $message_id, ['controller', 'params']);
- if($messages > 0) {
+ if($messages_ids > 0 && count($messages_ids)) {
+ $messages = $orm->read('core\alert\Message', $messages_ids, ['id', 'controller', 'params']);
+ if($messages > 0 && count($messages)) {
$message = reset($messages);
- $orm->delete('core\alert\Message', $message_id, true);
+ $orm->delete('core\alert\Message', $message['id'], true);
if($message['controller']) {
try {
$body = json_decode($message['params'], true);
\eQual::run('do', $message['controller'], $body, true);
}
catch(\Exception $e) {
- // error occured during execution
+ // error occurred during execution
}
}
}
diff --git a/lib/equal/error/Reporter.class.php b/lib/equal/error/Reporter.class.php
index 402232017..a49f78567 100644
--- a/lib/equal/error/Reporter.class.php
+++ b/lib/equal/error/Reporter.class.php
@@ -7,8 +7,6 @@
namespace equal\error;
use equal\organic\Service;
-use equal\php\Context;
-
class Reporter extends Service {
@@ -47,19 +45,21 @@ public static function constants() {
* In all cases, these are critical errors that cannot be recovered and need an immediate stop (fatal error)
*/
public static function uncaughtExceptionHandler($exception) {
- $code = $exception->getCode();
+ self::handleThrowable($exception);
+ // prevent further processing
+ exit(1);
+ }
+
+ public static function handleThrowable($exception) {
$msg = $exception->getMessage();
- if($code != QN_REPORT_FATAL) {
- $msg = '[uncaught exception]-'.$msg;
- }
// retrieve instance and log error
$instance = self::getInstance();
- $trace = $exception->getTrace();
- if(count($trace)) {
- $instance->log($code, $msg, $trace[0]);
+ $backtrace = $exception->getTrace();
+ if(count($backtrace)) {
+ $trace = array_shift($backtrace);
+ $trace['stack'] = $backtrace;
+ $instance->log(QN_REPORT_ERROR, $msg, $trace);
}
- // prevent processing
- exit(1);
}
/**
@@ -151,16 +151,16 @@ private function log($code, $msg, $trace) {
'mtime' => substr($time_parts[0], 2, 6),
'level' => qn_debug_code_name($code),
'mode' => qn_debug_mode_name($mode),
- 'class' => $trace['class'],
- 'function' => $trace['function'],
- 'file' => $trace['file'],
- 'line' => $trace['line'],
+ 'class' => (isset($trace['class']))?$trace['class']:'',
+ 'function' => (isset($trace['function']))?$trace['function']:'',
+ 'file' => (isset($trace['file']))?$trace['file']:'',
+ 'line' => (isset($trace['line']))?$trace['line']:'',
'message' => $msg,
- 'stack' => []
+ 'stack' => (isset($trace['stack']))?$trace['stack']:[]
];
// append backtrace if required (fatal errors)
- if(in_array($code, [QN_REPORT_WARNING, QN_REPORT_ERROR, QN_REPORT_FATAL])) {
+ if(!count($error_json['stack']) && in_array($code, [QN_REPORT_WARNING, QN_REPORT_ERROR, QN_REPORT_FATAL])) {
$stack = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 10);
// remove 2 calls related to Reporter Service
array_splice($stack, 0, 2);
@@ -243,4 +243,4 @@ public function debug($msg) {
$this->log(QN_REPORT_DEBUG, $msg, self::getTrace(2));
}
-}
\ No newline at end of file
+}
diff --git a/lib/equal/fs/FSManipulator.class.php b/lib/equal/fs/FSManipulator.class.php
index e878c1af7..88beff4c6 100644
--- a/lib/equal/fs/FSManipulator.class.php
+++ b/lib/equal/fs/FSManipulator.class.php
@@ -710,6 +710,21 @@ public static function assertPath($path) {
}
}
+ public static function getSanitizedPath($path) {
+ $banned = [
+ "%252e%252e%255c",
+ "%2e%2e%2f",
+ "%2e%2e%5c",
+ "%2e%2e/",
+ "..%255c",
+ "..%2f",
+ "..%5c",
+ "../",
+ "\0"
+ ];
+ return str_replace($banned, '', str_replace('\\', '/', $path));
+ }
+
public static function getSanitizedName($file_name) {
// note: remember to maintain current file charset to UTF-8 !
$ascii = array(
@@ -733,9 +748,43 @@ public static function getSanitizedName($file_name) {
return strtolower($value);
}
+ public static function getDirFlatten(string $path, $extensions=[], string $prefix = '') {
+ $res = [];
+ if($scan = scandir($path)) {
+ foreach($scan as $item) {
+ if(substr($item, 0, 1) == '.') {
+ continue;
+ }
+ if(is_file("$path/$item")) {
+ if(count($extensions)) {
+ foreach($extensions as $extension) {
+ $extension = str_replace('.', '', $extension);
+ $len = strlen($extension);
+ if(substr(strtolower($item), -($len+1)) == '.'.$extension) {
+ $res[] = $prefix.$item;
+ break;
+ }
+ }
+ }
+ else {
+ $res[] = $prefix.$item;
+ }
+ }
+ elseif(is_dir("$path/$item")) {
+ $res = array_merge($res, self::getDirFlatten("$path/$item", $extensions, $prefix.$item.'/'));
+ }
+ }
+ }
+ return $res;
+ }
+
public static function getDirListing($dir_name) {
- if(!is_dir($dir_name)) return null;
- if(($dir = opendir($dir_name)) === false) return null;
+ if(!is_dir($dir_name)) {
+ return null;
+ }
+ if(($dir = opendir($dir_name)) === false) {
+ return null;
+ }
$d_pos = 0;
$f_pos = 0;
$list_directoies = array();
diff --git a/lib/equal/orm/Collection.class.php b/lib/equal/orm/Collection.class.php
index e803bbcfe..dd58b14f0 100644
--- a/lib/equal/orm/Collection.class.php
+++ b/lib/equal/orm/Collection.class.php
@@ -73,9 +73,7 @@ public function __construct($class, $objectManager, $accessController, $authenti
// assign private members
$this->class = $class;
- /** @var \equal\orm\ObjectManager */
$this->orm = $objectManager;
- /** @var \equal\access\AccessController */
$this->ac = $accessController;
$this->am = $authenticationManager;
$this->dap = $dataAdapterProvider;
diff --git a/lib/equal/orm/ObjectManager.class.php b/lib/equal/orm/ObjectManager.class.php
index 5b2d9a95f..73b38b892 100644
--- a/lib/equal/orm/ObjectManager.class.php
+++ b/lib/equal/orm/ObjectManager.class.php
@@ -86,7 +86,7 @@ class ObjectManager extends Service {
'many2one' => array('description', 'help', 'type', 'visible', 'default', 'dependencies', 'readonly', 'required', 'deprecated', 'foreign_object', 'domain', 'onupdate', 'ondelete', 'multilang'),
'one2many' => array('description', 'help', 'type', 'visible', 'default', 'dependencies', 'readonly', 'deprecated', 'foreign_object', 'foreign_field', 'domain', 'onupdate', 'ondetach', 'order', 'sort'),
'many2many' => array('description', 'help', 'type', 'visible', 'default', 'dependencies', 'readonly', 'deprecated', 'foreign_object', 'foreign_field', 'rel_table', 'rel_local_key', 'rel_foreign_key', 'domain', 'onupdate'),
- 'computed' => array('description', 'help', 'type', 'visible', 'default', 'dependencies', 'readonly', 'deprecated', 'result_type', 'usage', 'function', 'onupdate', 'store', 'instant', 'multilang', 'selection', 'foreign_object')
+ 'computed' => array('description', 'help', 'type', 'visible', 'default', 'dependencies', 'readonly', 'deprecated', 'result_type', 'usage', 'function', 'onupdate', 'onrevert', 'store', 'instant', 'multilang', 'selection', 'foreign_object')
];
public static $mandatory_attributes = [
@@ -164,6 +164,7 @@ class ObjectManager extends Service {
'markup/html' => 'string(64000)', // 64k chars html
'text/html' => 'string(64000)',
'text/plain' => 'string(64000)', // 64k chars text
+ 'text/json' => 'string(64000)', // 64k chars json
'email' => 'string(255)',
'phone' => 'string(20)'
];
@@ -196,7 +197,7 @@ public static function constants() {
}
/**
- * #todo - deprecated : use the DBConnection service instead
+ * #todo - deprecate : use the DBConnection service instead
* @deprecated
*/
public function getDB() {
@@ -214,7 +215,7 @@ private function getDBHandler() {
if(!$this->db->connected()) {
if($this->db->connect() === false) {
// fatal error
- trigger_error('Unable to establish connection to database: check connection parameters '.
+ trigger_error("ORM::".'Unable to establish connection to database: check connection parameters '.
'(possibles reasons: non-supported DBMS, unknown database name, incorrect username or password, DB offline, ...)',
QN_REPORT_ERROR
);
@@ -309,7 +310,7 @@ public function getObjectTableName($class) {
$result = strtolower($object->getTable());
}
catch(Exception $e) {
- trigger_error($e->getMessage(), QN_REPORT_ERROR);
+ trigger_error("ORM::".$e->getMessage(), QN_REPORT_ERROR);
return $e->getCode();
}
return $result;
@@ -471,6 +472,8 @@ private function filterValidIdentifiers($class, $ids) {
}
// remove duplicate ids, if any
$ids = array_unique($ids);
+ // #removed #memo - this leads to a lot of extra individual DB requests
+ /*
// process remaining identifiers
$valid_ids = [];
if(!empty($ids)) {
@@ -491,6 +494,7 @@ private function filterValidIdentifiers($class, $ids) {
unset($ids[$i]);
}
}
+ */
return $ids;
}
@@ -633,7 +637,7 @@ private function load($class, $ids, $fields, $lang) {
null,
[
[
- // note :we have to escape right field because there is no way for dbManipulator to guess it is not a value
+ // #memo - right field needs to be escaped because there is no way for dbManipulator to guess it is not a value
array('t0.id', '=', "`t1`.`{$schema[$field]['rel_foreign_key']}`"),
array('t1.'.$schema[$field]['rel_local_key'], 'in', $ids),
array('t0.state', '=', 'instance'),
@@ -743,7 +747,7 @@ private function load($class, $ids, $fields, $lang) {
}
catch(Exception $e) {
- trigger_error($e->getMessage(), QN_REPORT_ERROR);
+ trigger_error("ORM::".$e->getMessage(), QN_REPORT_ERROR);
throw new Exception('unable to load object fields', $e->getCode());
}
}
@@ -828,9 +832,11 @@ private function store($class, $ids, $fields, $lang) {
$values_array
);
},
- 'simple' => function($om, $ids, $fields) use ($schema, $class, $table_name, $lang) {
+ 'simple' => function($om, $ids, $fields) use ($schema, $class, $table_name) {
/** @var \equal\data\adapt\DataAdapterProvider */
$dap = $this->container->get('adapt');
+ // #memo - this handler is for non-multilang fields
+ $lang = constant('DEFAULT_LANG');
foreach($ids as $oid) {
$fields_values = array();
foreach($fields as $field) {
@@ -848,7 +854,9 @@ private function store($class, $ids, $fields, $lang) {
$om->db->setRecords($om->getObjectTableName($class), array($oid), $fields_values);
}
},
- 'one2many' => function($om, $ids, $fields) use ($schema, $class, $table_name, $lang) {
+ 'one2many' => function($om, $ids, $fields) use ($schema, $class, $table_name) {
+ // #memo - this handler is for non-multilang fields
+ $lang = constant('DEFAULT_LANG');
foreach($ids as $oid) {
foreach($fields as $field) {
$value = $om->cache[$table_name][$oid][$lang][$field];
@@ -857,7 +865,7 @@ private function store($class, $ids, $fields, $lang) {
$value = [intval($value)];
}
else {
- trigger_error("wrong value for field '$field' of class '$class', should be an array", QN_REPORT_ERROR);
+ trigger_error("ORM::wrong value for field '$field' of class '$class', should be an array", QN_REPORT_ERROR);
continue;
}
}
@@ -899,13 +907,17 @@ private function store($class, $ids, $fields, $lang) {
}
}
// add relation by setting the pointing id (overwrite previous value if any)
- if(count($ids_to_add)) $om->db->setRecords($foreign_table, $ids_to_add, array($schema[$field]['foreign_field']=>$oid));
+ if(count($ids_to_add)) {
+ $om->db->setRecords($foreign_table, $ids_to_add, [$schema[$field]['foreign_field'] => $oid]);
+ }
// invalidate cache (field partially loaded)
unset($om->cache[$table_name][$oid][$lang][$field]);
}
}
},
- 'many2many' => function($om, $ids, $fields) use ($schema, $class, $table_name, $lang) {
+ 'many2many' => function($om, $ids, $fields) use ($schema, $class, $table_name) {
+ // #memo - this handler is for non-multilang fields
+ $lang = constant('DEFAULT_LANG');
foreach($ids as $oid) {
foreach($fields as $field) {
$rel_ids = $om->cache[$table_name][$oid][$lang][$field];
@@ -914,7 +926,7 @@ private function store($class, $ids, $fields, $lang) {
$rel_ids = [intval($rel_ids)];
}
else {
- trigger_error("wrong value for field '$field' of class '$class', should be an array", QN_REPORT_ERROR);
+ trigger_error("ORM::wrong value for field '$field' of class '$class', should be an array", QN_REPORT_ERROR);
continue;
}
}
@@ -980,9 +992,13 @@ private function store($class, $ids, $fields, $lang) {
$fields_lists['multilang'][] = $field;
}
// note: if $lang differs from DEFAULT_LANG and field is not set as multilang, no change will be stored for that field
- else $fields_lists['simple'][] = $field;
+ else {
+ $fields_lists['simple'][] = $field;
+ }
+ }
+ else {
+ $fields_lists[$type][] = $field;
}
- else $fields_lists[$type][] = $field;
}
// 2) store fields according to their types
foreach($fields_lists as $type => $list) {
@@ -991,7 +1007,7 @@ private function store($class, $ids, $fields, $lang) {
}
catch (Exception $e) {
- trigger_error($e->getMessage(), QN_REPORT_ERROR);
+ trigger_error("ORM::".$e->getMessage(), QN_REPORT_ERROR);
throw new Exception('unable to store object fields', $e->getCode());
}
}
@@ -1090,7 +1106,7 @@ public function callonce($class, $method, $ids, $values=[], $lang=null, $signatu
/**
* Invoke a callback from an object Class.
- * Objects callback signature must always be `methodName($orm: object, $ids: array, $lang: string)`
+ * Default objects callback signature is `methodName($orm: object, $ids: array, $lang: string)`
* There is no recursion protection and a same callback can be invoked several times without any restrictions.
*
* @param string $class
@@ -1187,7 +1203,7 @@ public function getLastError() {
* Checks whether a set of values is valid according to given class definition.
* This is done using the class validation method.
*
- * Ex.:
+ * Result example:
* "INVALID_PARAM": {
* "login": {
* "invalid_email": "Login must be a valid email address."
@@ -1241,6 +1257,9 @@ public function validate($class, $ids, $values, $check_unique=false, $check_requ
// $usage = $f->getUsage();
// $constraints = $f->getConstraints();
// foreach($constraints as $error_id => $constraint) {
+ // if(!isset($constraint['function'])) {
+ // continue;
+ // }
// $fn = $constraint['function'];
// if(is_callable($fn)) {
// $fn->bindTo($usage);
@@ -1267,7 +1286,9 @@ public function validate($class, $ids, $values, $check_unique=false, $check_requ
// #todo - continue this list
case 'text':
case 'text/plain':
+ case 'text/json':
case 'text/html':
+ case 'text/json':
$type = 'text';
break;
}
@@ -1491,19 +1512,24 @@ public function create($class, $fields=null, $lang=null, $use_draft=true) {
],
['id' => 'asc']
);
- if(count($ids) && $ids[0] > 0) {
+ if($ids > 0 && count($ids) && $ids[0] > 0) {
// use the oldest expired draft
$oid = $ids[0];
// store the id to reuse
$creation_array['id'] = $oid;
// and delete the associated record (might contain obsolete data)
$db->deleteRecords($table_name, array($oid));
+ trigger_error("ORM::found draft object in table $table_name with id $oid.", QN_REPORT_DEBUG);
+ }
+ else {
+ trigger_error("ORM::no reusable draft object found.", QN_REPORT_DEBUG);
}
}
}
else {
- $ids = $this->filterValidIdentifiers($class, [$creation_array['id']]);
- if(!empty($ids)) {
+ // check if there is an object with same id
+ $records = $db->getRecords($table_name, 'id', (array) $creation_array['id']);
+ if($db->fetchArray($records)) {
throw new Exception('duplicate_object_id', QN_ERROR_CONFLICT_OBJECT);
}
$oid = (int) $creation_array['id'];
@@ -1525,6 +1551,9 @@ public function create($class, $fields=null, $lang=null, $use_draft=true) {
if($oid <= 0) {
// id field is auto-increment: retrieve last value
$oid = $db->getLastId();
+ if($oid <= 0) {
+ throw new Exception('invalid_object_id', QN_ERROR_UNKNOWN);
+ }
$this->cache[$table_name][$oid][$lang] = $creation_array;
}
// in any case, we return the object id
@@ -1546,7 +1575,7 @@ public function create($class, $fields=null, $lang=null, $use_draft=true) {
}
catch(Exception $e) {
- trigger_error($e->getMessage(), QN_REPORT_WARNING);
+ trigger_error("ORM::".$e->getMessage(), QN_REPORT_WARNING);
$this->last_error = $e->getMessage();
$res = $e->getCode();
}
@@ -1568,7 +1597,7 @@ public function write($class, $ids=null, $fields=null, $lang=null, $create=false
* @param mixed $ids Identifier(s) of the object(s) to update (accepted types: array, integer, numeric string).
* @param mixed $fields Array mapping fields names with the value (PHP) to which they must be set.
* @param string $lang Language under which fields have to be stored (only relevant for multilang fields).
- * @param bool $create Flag to mark the call as originating from the create() method (disables the canupdate hook call).
+ * @param bool $create Flag to mark the call as originating from the create() method (disables the canupdate and events hooks call).
*
* @return int|array Returns an array of updated ids, or an error identifier in case an error occurred.
*/
@@ -1591,6 +1620,7 @@ public function update($class, $ids=null, $fields=null, $lang=null, $create=fals
$ids = $this->filterValidIdentifiers($class, $ids);
// if no ids were specified, the result is an empty list (array)
if(empty($ids)) {
+ trigger_error("ORM::ignoring call with empty ids ", QN_REPORT_INFO);
return $res;
}
// ids that are left are the ones of the objects that will be written
@@ -1628,6 +1658,7 @@ public function update($class, $ids=null, $fields=null, $lang=null, $create=fals
unset($fields_to_check[$field]);
}
}
+ // #todo - split the tests with status check against the object workflow
$canupdate = $this->callonce($class, 'canupdate', $ids, $fields_to_check, $lang);
if($canupdate > 0 && !empty($canupdate)) {
throw new \Exception(serialize($canupdate), QN_ERROR_NOT_ALLOWED);
@@ -1640,9 +1671,10 @@ public function update($class, $ids=null, $fields=null, $lang=null, $create=fals
// 4) call 'onupdate' hook : notify objects that they're about to be updated with given values
- // #todo - split the tests with status check against the object workflow
-
- $this->callonce($class, 'onupdate', $ids, $fields, $lang);
+ if(!$create) {
+ // #todo - allow explicit notation `onbeforeupdate()`
+ $this->callonce($class, 'onupdate', $ids, $fields, $lang);
+ }
// 5) update objects
@@ -1689,18 +1721,20 @@ public function update($class, $ids=null, $fields=null, $lang=null, $create=fals
// 7) second pass : handle onupdate events, if any
- // #memo - this must be done after modifications otherwise object values might be outdated
- if(count($onupdate_fields)) {
- // #memo - several onupdate callbacks can, in turn, trigger a same other callback, which must then be called as many times as necessary
- foreach($onupdate_fields as $field) {
- // run onupdate callback (ignore undefined methods)
- $this->callonce($class, $schema[$field]['onupdate'], $ids, $fields, $lang, ['ids', 'values', 'lang']);
+ if(!$create) {
+ // #memo - this must be done after modifications otherwise object values might be outdated
+ if(count($onupdate_fields)) {
+ // #memo - several onupdate callbacks can, in turn, trigger a same other callback, which must then be called as many times as necessary
+ foreach($onupdate_fields as $field) {
+ // run onupdate callback (ignore undefined methods)
+ $this->callonce($class, $schema[$field]['onupdate'], $ids, $fields, $lang, ['ids', 'values', 'lang']);
+ }
}
- }
- if(count($onrevert_fields)) {
- foreach($onrevert_fields as $field) {
- // run onupdate callback (ignore undefined methods)
- $this->callonce($class, $schema[$field]['onrevert'], $ids, $fields, $lang, ['ids', 'values', 'lang']);
+ if(count($onrevert_fields)) {
+ foreach($onrevert_fields as $field) {
+ // run onrevert callback (ignore undefined methods)
+ $this->callonce($class, $schema[$field]['onrevert'], $ids, $fields, $lang, ['ids', 'values', 'lang']);
+ }
}
}
@@ -1719,19 +1753,19 @@ public function update($class, $ids=null, $fields=null, $lang=null, $create=fals
}
// remember fields that must be re-computed instantly
$instant_fields = [];
- foreach($dependencies as $dependency) {
+ foreach(array_unique($dependencies) as $dependency) {
// #todo - add support for dot notation
if(isset($schema[$dependency]) && $schema[$dependency]['type'] == 'computed') {
if(isset($schema[$dependency]['instant']) && $schema[$dependency]['instant']) {
$instant_fields[] = $dependency;
}
// allow cascade update
- $this->update($class, $ids, [$dependency => null], $lang);
+ $this->update($class, $ids, [$dependency => null], $lang, $create);
}
}
if(count($instant_fields)) {
// re-compute 'instant' computed field
- $this->read($class, $ids, $instant_fields, $lang);
+ $this->read($class, $ids, array_unique($instant_fields), $lang);
}
@@ -1744,17 +1778,21 @@ public function update($class, $ids=null, $fields=null, $lang=null, $create=fals
$t_res = $this->transition($class, (array) $object_id, $transition);
// transition succeeded
if(count($t_res) == 0) {
- // there should be only one applicable transition, process next object
+ // there should be only one applicable transition: process next object
break;
}
}
}
+ if(!$create) {
+ $this->callonce($class, 'onafterupdate', $ids, $fields, $lang);
+ }
+
// #todo - move this to a dedicated controller for CRON
// 10) upon state update (to 'archived' or 'deleted'), remove any pending alert related to the object
if(isset($fields['state']) && !in_array($fields['state'], ['draft', 'instance'])) {
- $messages_ids = $this->search('core\alert\Message', [ ['object_class', '=', get_called_class()], ['object_id', 'in', $ids] ] );
+ $messages_ids = $this->search('core\alert\Message', [ ['object_class', '=', $class], ['object_id', 'in', $ids] ] );
if($messages_ids) {
$this->delete('core\alert\Message', $messages_ids, true);
}
@@ -1762,7 +1800,7 @@ public function update($class, $ids=null, $fields=null, $lang=null, $create=fals
}
catch(Exception $e) {
- trigger_error($e->getMessage(), QN_REPORT_ERROR);
+ trigger_error("ORM::".$e->getMessage(), QN_REPORT_ERROR);
$this->last_error = $e->getMessage();
$res = $e->getCode();
}
@@ -1885,7 +1923,7 @@ public function read($class, $ids=null, $fields=null, $lang=null) {
foreach($ids as $oid) {
if(!isset($this->cache[$table_name][$oid]) || empty($this->cache[$table_name][$oid])) {
- trigger_error("ORM::unknown or empty object $class[$oid]", QN_REPORT_WARNING);
+ trigger_error("ORM::unknown or empty object $class [$oid]", QN_REPORT_WARNING);
continue;
}
// first pass : retrieve fields values
@@ -1946,7 +1984,7 @@ public function read($class, $ids=null, $fields=null, $lang=null) {
}
}
catch(Exception $e) {
- trigger_error($e->getMessage(), QN_REPORT_ERROR);
+ trigger_error("ORM::".$e->getMessage(), QN_REPORT_ERROR);
$res = $e->getCode();
}
return $res;
@@ -1999,7 +2037,7 @@ public function delete($class, $ids, $permanent=false) {
}
// 3) call 'ondelete' hook : notify objects that they're about to be deleted
-
+ // #todo allow explicit notation 'onbeforedelete'
$this->callonce($class, 'ondelete', $ids, [], null, ['ids']);
// 4) cascade deletions / relations updates
@@ -2091,7 +2129,7 @@ public function delete($class, $ids, $permanent=false) {
$this->callonce($class, 'onafterdelete', $ids, [], null, ['ids']);
}
catch(Exception $e) {
- trigger_error($e->getMessage(), QN_REPORT_ERROR);
+ trigger_error("ORM::".$e->getMessage(), QN_REPORT_ERROR);
$this->last_error = $e->getMessage();
$res = $e->getCode();
}
@@ -2188,7 +2226,7 @@ public function clone($class, $id, $values=[], $lang=null, $parent_field='') {
}
catch(Exception $e) {
- trigger_error($e->getMessage(), QN_REPORT_ERROR);
+ trigger_error("ORM::".$e->getMessage(), QN_REPORT_ERROR);
$this->last_error = $e->getMessage();
$res = $e->getCode();
}
@@ -2632,7 +2670,7 @@ public function search($class, $domain=null, $sort=['id' => 'asc'], $start='0',
$res_list = array_unique($res_list);
}
catch(Exception $e) {
- trigger_error($e->getMessage(), QN_REPORT_ERROR);
+ trigger_error("ORM::".$e->getMessage(), QN_REPORT_ERROR);
$res_list = $e->getCode();
}
return $res_list;
diff --git a/lib/equal/orm/UsageFactory.class.php b/lib/equal/orm/UsageFactory.class.php
index 8fb57b5e4..1cbb06a1a 100644
--- a/lib/equal/orm/UsageFactory.class.php
+++ b/lib/equal/orm/UsageFactory.class.php
@@ -20,6 +20,7 @@
UsagePassword,
UsagePhone,
UsageText,
+ UsageUri,
UsageArray
};
@@ -105,6 +106,7 @@ public static function create(string $usage): Usage {
$usageInstance = new UsagePhone($usage);
break;
case 'uri':
+ $usageInstance = new UsageUri($usage);
break;
case 'array':
$usageInstance = new UsageArray($usage);
diff --git a/lib/equal/orm/usages/Usage.class.php b/lib/equal/orm/usages/Usage.class.php
index fd1e66c17..5ad2fef56 100644
--- a/lib/equal/orm/usages/Usage.class.php
+++ b/lib/equal/orm/usages/Usage.class.php
@@ -13,43 +13,43 @@
class Usage {
/** @var string */
- private $usage_str = '';
+ protected $usage_str = '';
/**
* Usage main type.
* @var string
*/
- private $type = '';
+ protected $type = '';
/**
* Usage subtype.
* Subtype might have several nodes (ex: node1.node2.node3)
* @var string
*/
- private $subtype = '';
+ protected $subtype = '';
/**
* Flag marking if the usage targets an array.
* @var boolean
*/
- private $is_array = false;
+ protected $is_array = false;
/** @var string
* Accepts various formats ({length} (ex.'255'), {precision}.{scale} (ex. '5:3'), or {shortcut} (ex. 'medium'))
*/
- private $length = '';
+ protected $length = '';
/** @var int */
- private $precision = 0;
+ protected $precision = 0;
/** @var int */
- private $scale = 0;
+ protected $scale = 0;
/**
* Size of the array (when usage targets an array of values)
* @var int
*/
- private $size = 0;
+ protected $size = 0;
public function getConstraints(): array {
return [];
@@ -109,6 +109,14 @@ public function getScale(): int {
/**
* @param string $usage_str Usage string: string describing the usage.
+ *
+ * @example
+ * urn/isbn.10
+ * number[3]/integer:2
+ * language/iso-639:3
+ * number/real:5.2
+ * date/weekday.mon:short
+ *
*/
public function __construct(string $usage_str) {
diff --git a/lib/equal/orm/usages/UsageDate.class.php b/lib/equal/orm/usages/UsageDate.class.php
index 26c9118f7..6c901b553 100644
--- a/lib/equal/orm/usages/UsageDate.class.php
+++ b/lib/equal/orm/usages/UsageDate.class.php
@@ -22,10 +22,11 @@ class UsageDate extends Usage {
date/yearweek
date/yearday (ISO-8601)
datetime (ISO 8601)
- time/plain (h:m:s)
*/
public function getConstraints(): array {
- switch($this->getSubtype()) {
+ $subtype = $this->getSubtype();
+ $main_subtype = ( explode('.', $subtype) )[0];
+ switch($main_subtype) {
case 'day':
return [
'invalid_amount' => [
@@ -66,6 +67,7 @@ public function getConstraints(): array {
]
];
}
+ return [];
}
}
diff --git a/lib/equal/orm/usages/UsageText.class.php b/lib/equal/orm/usages/UsageText.class.php
index 363e4e950..56a82cec2 100644
--- a/lib/equal/orm/usages/UsageText.class.php
+++ b/lib/equal/orm/usages/UsageText.class.php
@@ -9,6 +9,13 @@
class UsageText extends Usage {
+ public function __construct(string $usage_str) {
+ parent::__construct($usage_str);
+ if($this->length == 0) {
+ $this->length = 32000;
+ }
+ }
+
public function getConstraints(): array {
return [
'not_string_type' => [
diff --git a/lib/equal/orm/usages/UsageURI.class.php b/lib/equal/orm/usages/UsageUri.class.php
similarity index 97%
rename from lib/equal/orm/usages/UsageURI.class.php
rename to lib/equal/orm/usages/UsageUri.class.php
index 3603d1e5e..000fb0cbf 100644
--- a/lib/equal/orm/usages/UsageURI.class.php
+++ b/lib/equal/orm/usages/UsageUri.class.php
@@ -32,7 +32,7 @@ public function getConstraints(): array {
}
]
];
- case 'uri/url.tel':
+ case 'url.tel':
return [
'invalid_url' => [
'message' => 'String is not a valid tel URL.',
@@ -41,7 +41,7 @@ public function getConstraints(): array {
}
]
];
- case 'uri/url.mailto':
+ case 'url.mailto':
return [
'invalid_url' => [
'message' => 'String is not a valid mailto URL.',
diff --git a/packages/core/actions/alert/bulk-dismiss.php b/packages/core/actions/alert/bulk-dismiss.php
new file mode 100644
index 000000000..6509584ad
--- /dev/null
+++ b/packages/core/actions/alert/bulk-dismiss.php
@@ -0,0 +1,48 @@
+
+ Some Rights Reserved, Yesbabylon SRL, 2020-2021
+ Licensed under GNU AGPL 3 license
+*/
+use core\alert\Message;
+
+list($params, $providers) = eQual::announce([
+ 'description' => "Tries to dismiss a selection of alert message. Should be invoked as a user request for removing the message.",
+ 'params' => [
+ 'ids' => [
+ 'description' => 'Identifiers of the alerts to dismiss.',
+ 'type' => 'one2many',
+ 'foreign_object' => 'core\alert\Message',
+ 'required' => true
+ ]
+ ],
+ 'access' => [
+ 'visibility' => 'protected'
+ ],
+ 'response' => [
+ 'content-type' => 'application/json',
+ 'charset' => 'utf-8',
+ 'accept-origin' => '*'
+ ],
+ 'providers' => ['context', 'dispatch']
+]);
+
+/**
+ * @var \equal\php\Context $context
+ * @var \equal\dispatch\Dispatcher $dispatch
+ */
+list($context, $dispatch) = [ $providers['context'], $providers['dispatch']];
+
+$messages = Message::ids($params['ids'])->read(['id']);
+
+foreach($messages as $id => $message) {
+ try {
+ eQual::run('do', 'core_alert_dismiss', ['id' => $id]);
+ }
+ catch(Exception $e) {
+ // something went wrong : ignore
+ }
+}
+
+$context->httpResponse()
+ ->send();
diff --git a/packages/core/actions/alert/dismiss.php b/packages/core/actions/alert/dismiss.php
index 4fd073321..ee5828fa0 100644
--- a/packages/core/actions/alert/dismiss.php
+++ b/packages/core/actions/alert/dismiss.php
@@ -4,12 +4,9 @@
Some Rights Reserved, Yesbabylon SRL, 2020-2021
Licensed under GNU AGPL 3 license
*/
-use sale\booking\Consumption;
-use lodging\sale\booking\BookingLine;
-use lodging\sale\booking\Booking;
-list($params, $providers) = announce([
- 'description' => "Tries to dismiss a message. Should be invoked as a user request for removing the message. If the situation is still occuring an identical alert will be re-created.",
+list($params, $providers) = eQual::announce([
+ 'description' => "Tries to dismiss a message. Should be invoked as a user request for removing the message. If the situation is still occurring an identical alert will be re-created.",
'params' => [
'id' => [
'description' => 'Identifier of the alert to dismiss.',
@@ -25,29 +22,22 @@
'charset' => 'utf-8',
'accept-origin' => '*'
],
- 'providers' => ['context', 'orm', 'auth', 'dispatch']
+ 'providers' => ['context', 'auth', 'dispatch']
]);
/**
* @var \equal\php\Context $context
- * @var \equal\orm\ObjectManager $orm
* @var \equal\auth\AuthenticationManager $auth
* @var \equal\dispatch\Dispatcher $dispatch
*/
-list($context, $orm, $auth, $dispatch) = [ $providers['context'], $providers['orm'], $providers['auth'], $providers['dispatch']];
+list($context, $auth, $dispatch) = [ $providers['context'], $providers['auth'], $providers['dispatch']];
-// #todo - restrict access based on link between messagemodel and user groups
-
-// #todo - deprecate (visibility = protected)
$user_id = $auth->userId();
-if($user_id <= 0) {
- throw new Exception("not_allowed", QN_ERROR_NOT_ALLOWED);
-}
+// #todo - restrict access based on link between MessageModel and user groups
// If the alert is not found, the call is ignored. If a controller is mentioned in the alert it is called.
$dispatch->dismiss($params['id']);
$context->httpResponse()
- ->status(200)
- ->send();
\ No newline at end of file
+ ->send();
diff --git a/packages/core/actions/config/create-model.php b/packages/core/actions/config/create-model.php
old mode 100755
new mode 100644
diff --git a/packages/core/actions/config/create-package.php b/packages/core/actions/config/create-package.php
index bc2abb999..90879b66c 100644
--- a/packages/core/actions/config/create-package.php
+++ b/packages/core/actions/config/create-package.php
@@ -63,6 +63,14 @@
throw new Exception("directory_creation_failed", QN_ERROR_UNKNOWN);
}
+if(!mkdir(QN_BASEDIR.'/packages/'.$params['package']."/init/data", 0775)) {
+ throw new Exception("directory_creation_failed", QN_ERROR_UNKNOWN);
+}
+
+if(!mkdir(QN_BASEDIR.'/packages/'.$params['package']."/init/demo", 0775)) {
+ throw new Exception("directory_creation_failed", QN_ERROR_UNKNOWN);
+}
+
if(!mkdir(QN_BASEDIR.'/packages/'.$params['package']."/i18n", 0775)) {
throw new Exception("directory_creation_failed", QN_ERROR_UNKNOWN);
}
@@ -71,6 +79,11 @@
throw new Exception("directory_creation_failed", QN_ERROR_UNKNOWN);
}
+if(!mkdir(QN_BASEDIR.'/packages/'.$params['package']."/uml", 0775)) {
+ throw new Exception("directory_creation_failed", QN_ERROR_UNKNOWN);
+}
+
+
// create empty manifest (from template)
$template = << '*'
],
'params' => [
- 'package' => [
+ 'package' => [
'type' => 'string',
'required' => true,
- 'selection' => array_combine(array_values($packages), array_values($packages))
+ 'selection' => array_values($packages)
],
'file' => [
'type' => 'string',
diff --git a/packages/core/actions/config/create-view.php b/packages/core/actions/config/create-view.php
index b6b8b89cc..3023c065f 100644
--- a/packages/core/actions/config/create-view.php
+++ b/packages/core/actions/config/create-view.php
@@ -56,10 +56,6 @@
throw new Exception("view_id_invalid",QN_ERROR_INVALID_PARAM);
}
-if(strcmp($type, "form")!==0 && strcmp($type, "list")!==0) {
- $test = strcmp($type, "list");
- throw new Exception("view_type_invalid",QN_ERROR_INVALID_PARAM);
-}
$file = QN_BASEDIR."/packages/{$package}/views/{$entity}.{$type}.{$name}.json";
@@ -85,10 +81,10 @@
throw new Exception('file_access_denied', QN_ERROR_UNKNOWN);
}
-if($type == "form") {
+if($type == "form" || $type == "search") {
fputs($f,"{\"layout\" : {\"groups\" : []}}");
}
-elseif($type == "list") {
+else {
fputs($f,"{\"layout\" : {\"items\" : []}}");
}
diff --git a/packages/core/actions/config/create-workflow.php b/packages/core/actions/config/create-workflow.php
new file mode 100644
index 000000000..ed98cd6d1
--- /dev/null
+++ b/packages/core/actions/config/create-workflow.php
@@ -0,0 +1,102 @@
+
+ Some Rights Reserved, Cedric Francoys, 2010-2021
+ Licensed under GNU LGPL 3 license
+*/
+use PhpParser\ParserFactory;
+
+list($params, $providers) = eQual::announce([
+ 'description' => "Add an empty workflow to the given class by creating a `getWorkflow()` method (if not defined yet).",
+ 'params' => [
+ 'entity' => [
+ 'description' => 'Full name (including namespace) of the class to look into (e.g. \'core\\User\').',
+ 'type' => 'string',
+ 'required' => true
+ ]
+ ],
+ 'response' => [
+ 'content-type' => 'text',
+ 'charset' => 'utf-8',
+ 'accept-origin' => '*'
+ ],
+ 'access' => [
+ 'visibility' => 'protected',
+ 'groups' => ['admins']
+ ],
+ 'providers' => ['context', 'orm']
+]);
+
+/**
+ * @var \equal\php\Context $context
+ * @var \equal\orm\ObjectManager $orm
+ */
+list($context, $orm) = [ $providers['context'], $providers['orm'] ];
+
+
+// retrieve target entity
+$entity = $orm->getModel($params['entity']);
+if(!$entity) {
+ throw new Exception("unknown_entity", QN_ERROR_INVALID_PARAM);
+}
+
+$reflectionClass = new ReflectionClass($entity::getType());
+if($reflectionClass->getMethod('getWorkflow')->class == $entity::getType()) {
+ throw new Exception("duplicate_method", QN_ERROR_INVALID_PARAM);
+}
+
+$parts = explode('\\', $params['entity']);
+// Get the package name from the first part of the string
+$package = array_shift($parts);// Get the package name from the first part of the string
+// Get the file name from the last part of the string
+$class_name = array_pop($parts);
+// Get the class path from the remaining part
+$class_path = implode('/', $parts);
+
+// Create all the object to use for using PhpParser
+$parser = (new ParserFactory)->create(ParserFactory::PREFER_PHP7);
+$printer = new PhpParser\PrettyPrinter\Standard;
+
+// Get the full path of the file
+$file = QN_BASEDIR."/packages/{$package}/classes/{$class_path}/{$class_name}.class.php";
+
+// get php code from original file ...
+$code = file_get_contents($file);
+
+// find the closing curly-bracket of the class
+$pos = strrpos($code, '}');
+
+if($pos === false) {
+ throw new Exception('malformed_file', QN_ERROR_UNKNOWN);
+}
+
+$code = substr_replace($code, 'public static function getWorkflow() {return [];} }', $pos, 1);
+
+// ... and parse it to create an AST
+$stmt = $parser->parse($code);
+
+// Pretty print the modified AST ...
+$result = $printer->prettyPrintFile($stmt);
+
+// ... and write back the code to the file
+if(file_put_contents($file, $result) === false) {
+ throw new Exception('io_error', QN_ERROR_UNKNOWN);
+}
+
+try {
+ // apply coding standards (ecs.php is expected in QN_BASEDIR)
+ $command = 'php ./vendor/bin/ecs check "' . str_replace('\\', '/', $file) . '" --fix';
+ if(exec($command) === false) {
+ throw new Exception('command_failed', QN_ERROR_UNKNOWN);
+ }
+}
+catch(Exception $e) {
+ trigger_error("PHP::unable to beautify rendered file ($file): ".$e->getMessage(), QN_REPORT_INFO);
+}
+
+$result = file_get_contents($file);
+
+$context->httpResponse()
+ ->status(204)
+ ->body($result)
+ ->send();
diff --git a/packages/core/actions/config/delete-view.php b/packages/core/actions/config/delete-view.php
index 1a62955ba..aa3bd41fb 100644
--- a/packages/core/actions/config/delete-view.php
+++ b/packages/core/actions/config/delete-view.php
@@ -4,8 +4,8 @@
Some Rights Reserved, Cedric Francoys, 2010-2021
Licensed under GNU LGPL 3 license
*/
-list($params, $providers) = announce([
- 'description' => "Saves a representation of a view to a json file.",
+list($params, $providers) = eQual::announce([
+ 'description' => "Removes the specified view relating to the given entity.",
'response' => [
'content-type' => 'text/plain',
'charset' => 'UTF-8',
diff --git a/packages/core/actions/config/delete-workflow.php b/packages/core/actions/config/delete-workflow.php
new file mode 100644
index 000000000..1ffd37f32
--- /dev/null
+++ b/packages/core/actions/config/delete-workflow.php
@@ -0,0 +1,94 @@
+
+ Some Rights Reserved, Cedric Francoys, 2010-2021
+ Licensed under GNU LGPL 3 license
+*/
+use PhpParser\{Node, NodeTraverser, NodeVisitorAbstract, ParserFactory, NodeFinder};
+
+list($params, $providers) = eQual::announce([
+ 'description' => "Removes the specified view relating to the given entity.",
+ 'response' => [
+ 'content-type' => 'text/plain',
+ 'charset' => 'UTF-8',
+ 'accept-origin' => '*'
+ ],
+ 'params' => [
+ 'entity' => [
+ 'description' => 'Name of the entity.',
+ 'type' => 'string',
+ 'required' => true
+ ]
+ ],
+ 'access' => [
+ 'visibility' => 'protected',
+ 'groups' => ['admins']
+ ],
+ 'providers' => ['context']
+]);
+
+/**
+ * @var \equal\php\Context $context
+ * @var \equal\orm\ObjectManager $orm
+ */
+list($context, $orm) = [$providers['context'], $providers['orm']];
+
+// Create all the object to use for using PhpParser
+$parser = (new ParserFactory)->create(ParserFactory::PREFER_PHP7);
+$nodeFinder = new NodeFinder;
+$traverser = new NodeTraverser;
+$prettyPrinter = new PhpParser\PrettyPrinter\Standard;
+// Get the parts of the entity string, separated by backslashes
+$parts = explode('\\', $params['entity']);
+// Get the package name from the first part of the string
+$package = array_shift($parts);// Get the package name from the first part of the string
+// Get the file name from the last part of the string
+$filename = array_pop($parts);
+// Get the class path from the remaining part
+$class_path = implode('/', $parts);
+
+// Add a visitor hijack the getWorkflow method and update its content with new schema
+$traverser->addVisitor(
+ new class("getWorkflow") extends NodeVisitorAbstract {
+ private $target;
+ public function __construct($target) {
+ $this->target = $target;
+ }
+ public function leaveNode(Node $node) {
+ if ($node->name->name === $this->target) {
+ return NodeTraverser::REMOVE_NODE;
+ }
+ }
+ }
+ );
+
+// Get the full path of the file
+$file = QN_BASEDIR."/packages/{$package}/classes/{$class_path}/{$filename}.class.php";
+// Get the code from the original file ...
+$code = file_get_contents($file);
+// ... and parse it to create an AST
+$stmtOriginal = $parser->parse($code);
+// Update the AST by using visitors attached to the traverser
+$stmtModified = $traverser->traverse($stmtOriginal);
+// Pretty print the modified AST ...
+$result = $prettyPrinter->prettyPrintFile($stmtModified);
+// ... and write back the code to the file
+if(file_put_contents($file, $result) === false) {
+ throw new Exception('io_error', QN_ERROR_UNKNOWN);
+}
+
+try {
+ // apply coding standards (ecs.php is expected in QN_BASEDIR)
+ $command = 'php ./vendor/bin/ecs check "' . str_replace('\\', '/', $file) . '" --fix';
+ if(exec($command) === false) {
+ throw new Exception('command_failed', QN_ERROR_UNKNOWN);
+ }
+}
+catch(Exception $e) {
+ trigger_error("PHP::unable to beautify rendered file ($file): ".$e->getMessage(), QN_REPORT_INFO);
+}
+
+$result = file_get_contents($file);
+$context->httpResponse()
+ ->body($result)
+ ->send();
diff --git a/packages/core/actions/config/update-controller.php b/packages/core/actions/config/update-controller.php
index 9e9571c48..1c7c31f1c 100644
--- a/packages/core/actions/config/update-controller.php
+++ b/packages/core/actions/config/update-controller.php
@@ -22,6 +22,7 @@
'controller' => [
'description' => 'Name of the controller.',
'type' => 'string',
+ 'usage' => 'orm/entity'
// 'required' => true
],
'operation' => [
@@ -55,8 +56,8 @@
$prettyPrinter = new PhpParser\PrettyPrinter\Standard;
// Get the parts of the entity string, separated by backslashes
-$params['entity'] = str_replace('_', '\\', $params['entity']);
-$parts = explode('\\', $params['entity']);
+$params['controller'] = str_replace('_', '\\', $params['controller']);
+$parts = explode('\\', $params['controller']);
// Get the package name from the first part of the string
$package = array_shift($parts);
@@ -65,9 +66,12 @@
// Get the class path from the remaining part
$class_path = implode('/', $parts);
+/*if(!($decoded = json_decode($params['payload'],true))) {
+ throw new Exception('Malformed Json', QN_ERROR_INVALID_PARAM);
+}*/
// Get a string representation from the code_php variable, with backslashes escaped
-$code_string = str_replace("\\\\", "\\", var_export($params['payload'], true));
+$code_string = str_replace("\\\\", "\\", var_export( $params['payload'], true));
// #test #toremove
// $code_string = "[
@@ -137,9 +141,10 @@ public function leaveNode(Node $node) {
);
// Get the full path of the file
-$dir = ['do' => 'actions', 'get' => 'date', 'show' => 'apps'][$params['operation']];
+$dir = ['do' => 'actions', 'get' => 'data', 'show' => 'apps'][$params['operation']];
$file = QN_BASEDIR."/packages/{$package}/{$dir}/{$class_path}/{$filename}.php";
+$file = str_replace("//","/",$file);
// Get the code from the original file ...
$code = file_get_contents($file);
// ... and parse it to create an AST
@@ -174,7 +179,8 @@ public function leaveNode(Node $node) {
}
}
catch(Exception $e) {
- trigger_error("PHP::unable to beautify rendered file ($file): ".$e->getMessage(), QN_REPORT_INFO);
+ throw new Exception('unable to beautfy the file', QN_ERROR_UNKNOWN);
+ //trigger_error("PHP::unable to beautify rendered file ($file): ".$e->getMessage(), QN_REPORT_INFO);
}
$result = file_get_contents($file);
diff --git a/packages/core/actions/config/update-init-data.php b/packages/core/actions/config/update-init-data.php
new file mode 100644
index 000000000..b152ccdd9
--- /dev/null
+++ b/packages/core/actions/config/update-init-data.php
@@ -0,0 +1,99 @@
+
+ Some Rights Reserved, Cedric Francoys, 2010-2021
+ Licensed under GNU LGPL 3 license
+*/
+use equal\fs\FSManipulator;
+
+list($params, $providers) = eQual::announce([
+ 'description' => "Save a representation of a view to a json file.",
+ 'response' => [
+ 'content-type' => 'text/plain',
+ 'charset' => 'UTF-8',
+ 'accept-origin' => '*'
+ ],
+ 'params' => [
+ 'package' => [
+ 'type' => 'string',
+ 'description' => 'Name of the entity.',
+ 'required' => true
+ ],
+ 'type' => [
+ 'type' => 'string',
+ 'description' => 'Type of requested init-data (folder).',
+ 'selection' => [
+ 'init',
+ 'demo'
+ ],
+ 'default' => 'init',
+ ],
+ 'payload' => [
+ 'description' => 'View definition (JSON).',
+ 'type' => 'text',
+ 'required' => true
+ ]
+ ],
+ 'providers' => ['context']
+]);
+
+/** @var \equal\php\context Context */
+list($context) = [$providers['context']];
+
+if( ($decoded = json_decode($params['payload'], true)) === null) {
+ throw new Exception("invalid_payload", QN_ERROR_INVALID_PARAM);
+}
+
+$package = $params['package'];
+
+$packages = equal::run("get", "config_packages");
+
+if(!in_array($package, $packages)) {
+ throw new Exception("unknown_package", QN_ERROR_INVALID_PARAM);
+}
+
+$folder = ([
+ "init" => "data",
+ "demo" => "demo"
+ ])[$params['type']];
+
+$dir = QN_BASEDIR."/packages/$package/init/$folder";
+
+if(!is_dir($dir)) {
+ throw new Exception("missing_directory", QN_ERROR_INVALID_CONFIG);
+}
+
+$files = FSManipulator::getDirFlatten($dir, ['json']);
+
+$backups = [];
+
+foreach($files as $file) {
+ unlink($dir.'/'.$file.'.bak');
+ $res = rename($dir.'/'.$file, $dir.'/'.$file.'.bak');
+ if(!$res) {
+ throw new Exception("io_error",QN_ERROR_INVALID_CONFIG);
+ }
+ $backups[] = $dir.'/'.$file.'.bak';
+}
+
+foreach($decoded as $file => $content) {
+ $sanitized_filename = FSManipulator::getSanitizedPath($file);
+ $f = fopen($dir.'/'.$sanitized_filename, "w");
+ if(!$f) {
+ throw new Exception("io_error", QN_ERROR_INVALID_CONFIG);
+ }
+ $json = json_encode($content, JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES|JSON_PRETTY_PRINT);
+ if(!$json) {
+ throw new Exception("encoding_error", QN_ERROR_INVALID_CONFIG);
+ }
+ fputs($f, $json);
+ fclose($f);
+}
+
+foreach($backups as $file) {
+ unlink($file);
+}
+
+$context->httpResponse()
+ ->status(201)
+ ->send();
diff --git a/packages/core/actions/config/update-model.php b/packages/core/actions/config/update-model.php
index 9f1c5aa12..a1ca6ac2a 100644
--- a/packages/core/actions/config/update-model.php
+++ b/packages/core/actions/config/update-model.php
@@ -6,6 +6,7 @@
*/
use PhpParser\{Node, NodeTraverser, NodeVisitorAbstract, ParserFactory, NodeFinder, NodeDumper, PrettyPrinter, BuilderFactory, Comment};
use equal\orm\Model;
+
list($params, $providers) = eQual::announce([
'description' => "Translate an entity definition to a PHP file and store it in related package dir.",
'help' => "This controller rely on the PHP binary. In order to make them work, sure the PHP binary is present in the PATH.",
@@ -20,16 +21,6 @@
'type' => 'string',
'required' => true
],
- 'part' => [
- 'description' => '',
- 'type' => 'string',
- 'selection' => [
- 'class', // #todo - replace with 'columns'
- 'workflow',
- 'roles'
- ],
- 'required' => true
- ],
'payload' => [
'description' => 'Entity definition .',
'type' => 'array',
@@ -38,11 +29,13 @@
],
'providers' => ['context', 'orm']
]);
+
/**
* @var \equal\php\Context $context
* @var \equal\orm\ObjectManager $orm
*/
list($context, $orm) = [$providers['context'], $providers['orm']];
+
// Create all the object to use for using PhpParser
$parser = (new ParserFactory)->create(ParserFactory::PREFER_PHP7);
$nodeFinder = new NodeFinder;
@@ -56,50 +49,52 @@
$filename = array_pop($parts);
// Get the class path from the remaining part
$class_path = implode('/', $parts);
+
$getColumns = null;
$code_php = null;
-// If you want to use save-model for updating a class/getColumns
-if($params['part'] == 'class') {
- // Decode the JSON string to a PHP object
- $code_php = sanitizeColumns($params['payload']['fields']);
- // Get a string representation from the code_php variable, with backslashes escaped
- $code_string = str_replace("\\\\", "\\", var_export($code_php, true));
- // Create a virtual file holing the default structure with the parsed code and generate a minimal AST
- $ast_temp = $parser->parse("findFirst($ast_temp, function(Node $node) {return $node->name->name === "getColumns";});
- // Add a visitor to the traverse in order to consider the getColumns method and update its content with new schema
- $traverser->addVisitor(
- new class($nodeGetColumns, "getColumns") extends NodeVisitorAbstract {
- private $node;
- private $target;
- public function __construct($node, $target) {
- $this->node = $node;
- $this->target = $target;
- }
- public function leaveNode(Node $node) {
- if ($node->name->name === $this->target) {
- return $this->node;
- }
- }
+
+// Decode the JSON string to a PHP object
+$code_php = sanitizeColumns($params['payload']['fields']);
+// Get a string representation from the code_php variable, with backslashes escaped
+$code_string = str_replace("\\\\", "\\", var_export($code_php, true));
+// Create a virtual file holing the default structure with the parsed code and generate a minimal AST
+$ast_temp = $parser->parse("findFirst($ast_temp, function(Node $node) {return $node->name->name === "getColumns";});
+
+// Add a visitor hijack the getColumns method and update its content with new schema
+$traverser->addVisitor(
+ new class($nodeGetColumns, "getColumns") extends NodeVisitorAbstract {
+ private $node;
+ private $target;
+ public function __construct($node, $target) {
+ $this->node = $node;
+ $this->target = $target;
}
- );
- // Add a visitor to the traverser for setting the doc comments on the class node
- $traverser->addVisitor(
- new class($code_php) extends NodeVisitorAbstract {
- private $code_php;
- public function __construct($code_php) {
- $this->code_php = $code_php;
+ public function leaveNode(Node $node) {
+ if ($node->name->name === $this->target) {
+ return $this->node;
}
- public function leaveNode(Node $node) {
- if ($node instanceof Node\Stmt\Class_) {
- $node->setDocComment(new Comment\Doc(getPropertiesAsComments($this->code_php)));
- return NodeTraverser::STOP_TRAVERSAL;
- }
+ }
+ }
+ );
+
+// Add a visitor for setting the doc comments on the class node
+$traverser->addVisitor(
+ new class($code_php) extends NodeVisitorAbstract {
+ private $code_php;
+ public function __construct($code_php) {
+ $this->code_php = $code_php;
+ }
+ public function leaveNode(Node $node) {
+ if ($node instanceof Node\Stmt\Class_) {
+ $node->setDocComment(new Comment\Doc(getPropertiesAsComments($this->code_php)));
+ return NodeTraverser::STOP_TRAVERSAL;
}
}
- );
-}
+ }
+ );
+
// Get the full path of the file
$file = QN_BASEDIR."/packages/{$package}/classes/{$class_path}/{$filename}.class.php";
// Get the code from the original file ...
@@ -112,20 +107,23 @@ public function leaveNode(Node $node) {
$result = $prettyPrinter->prettyPrintFile($stmtModified);
// ... and write back the code to the file
file_put_contents($file, $result);
+
try {
// apply coding standards (ecs.php is expected in QN_BASEDIR)
$command = 'php ./vendor/bin/ecs check "' . str_replace('\\', '/', $file) . '" --fix';
if(exec($command) === false) {
throw new Exception('command_failed', QN_ERROR_UNKNOWN);
}
+ $result = file_get_contents($file);
}
catch(Exception $e) {
trigger_error("PHP::unable to beautify rendered file ($file): ".$e->getMessage(), QN_REPORT_INFO);
}
-$result = file_get_contents($file);
+
$context->httpResponse()
->body($result)
->send();
+
/**
* Filter out the properties of the schema from special columns inherited from the Model.
*
@@ -146,6 +144,7 @@ function sanitizeColumns($properties) {
}
return $result;
}
+
/**
* Generate the PHP comments for the properties of the model.
*
@@ -181,4 +180,4 @@ function getPropertiesAsComments($properties) {
}
$comment .= "*/";
return $comment;
-}
\ No newline at end of file
+}
diff --git a/packages/core/actions/config/update-package.php b/packages/core/actions/config/update-package.php
new file mode 100644
index 000000000..b39878855
--- /dev/null
+++ b/packages/core/actions/config/update-package.php
@@ -0,0 +1,40 @@
+
+ Some Rights Reserved, Cedric Francoys, 2010-2021
+ Licensed under GNU LGPL 3 license
+*/
+
+list($params, $providers) = eQual::announce([
+ 'description' => "Updates a given package name.",
+ 'help' => "Still not sure how to handle this since the package name might be referenced in many places.",
+ 'response' => [
+ 'content-type' => 'text/plain',
+ 'charset' => 'UTF-8',
+ 'accept-origin' => '*'
+ ],
+ 'params' => [
+ 'package' => [
+ 'description' => 'Name of the package for which the name has to be updated.',
+ 'type' => 'string',
+ 'required' => true
+ ],
+ 'name' => [
+ 'description' => 'New name for the package.',
+ 'type' => 'string',
+ 'required' => true
+ ]
+ ],
+ 'providers' => ['context', 'orm']
+]);
+
+/**
+ * @var \equal\php\Context $context
+ * @var \equal\orm\ObjectManager $orm
+ */
+list($context, $orm) = [$providers['context'], $providers['orm']];
+
+
+$context->httpResponse()
+ ->body($result)
+ ->send();
diff --git a/packages/core/actions/config/update-translation.php b/packages/core/actions/config/update-translation.php
old mode 100755
new mode 100644
diff --git a/packages/core/actions/config/update-uml.php b/packages/core/actions/config/update-uml.php
new file mode 100644
index 000000000..78d2a4aca
--- /dev/null
+++ b/packages/core/actions/config/update-uml.php
@@ -0,0 +1,106 @@
+ "Attempts to create a new package using a given name.",
+ 'params' => [
+ 'package' => [
+ 'description' => 'Name of the package of the new model',
+ 'type' => 'string',
+ 'required' => true
+ ],
+ 'path' => [
+ 'decription' => 'relative path to the file from packages/{pkg}/',
+ 'type' => 'string',
+ 'required' => true
+ ],
+ 'payload' => [
+ 'description' => 'value of the field',
+ 'type' => 'text',
+ 'required' => true
+ ],
+ 'type' => [
+ 'description' => 'Type of the UML data',
+ 'type' => 'string',
+ 'required' => true,
+ 'selection' => [
+ 'or'
+ ]
+ ]
+ ],
+ 'response' => [
+ 'content-type' => 'text',
+ 'charset' => 'utf-8',
+ 'accept-origin' => '*'
+ ],
+ 'access' => [
+ 'visibility' => 'protected',
+ 'groups' => ['admins']
+ ],
+ 'providers' => ['context', 'orm', 'access']
+]);
+
+/**
+ * @var \equal\php\Context $context
+ * @var \equal\orm\ObjectManager $orm
+ * @var \equal\access\AccessController $ac
+ */
+list($context, $orm, $ac) = [$providers['context'], $providers['orm'], $providers['access']];
+
+$response_code = 200;
+
+$package = $params["package"];
+
+// Checking if package exists
+if(!file_exists(QN_BASEDIR."/packages/{$package}")) {
+ throw new Exception('missing_package_dir', QN_ERROR_INVALID_PARAM);
+}
+
+// Checking if package exists
+if(!file_exists(QN_BASEDIR."/packages/{$package}/uml")) {
+ throw new Exception('malformed_package', QN_ERROR_INVALID_CONFIG);
+}
+
+$path_arr = explode('/',$params['path']);
+$filename = array_pop($path_arr);
+$path = implode("/",$path_arr);
+
+$path = str_replace("..","",$path);
+
+$str_payload =$params['payload'];
+
+if(!endsWith($filename,".{$params["type"]}.equml")) {
+ $filename = $filename.".{$params["type"]}.equml";
+}
+
+if(!is_dir(QN_BASEDIR."/packages/{$package}/uml/{$path}")) {
+ $response_code = 201;
+ if(!mkdir(QN_BASEDIR."/packages/{$package}/uml/{$path}",0775,true)) {
+ throw new Exception('io_error'.QN_BASEDIR."/packages/{$package}/uml/{$path}", QN_ERROR_INVALID_CONFIG);
+ }
+}
+
+if($response_code === 200 && !file_exists(QN_BASEDIR."/packages/{$package}/uml/{$path}/{$filename}")) {
+ $response_code = 201;
+}
+// Create file
+$f = fopen(QN_BASEDIR."/packages/{$package}/uml/{$path}/{$filename}","w");
+if(!$f) {
+ throw new Exception('io_error', QN_ERROR_INVALID_CONFIG);
+}
+fputs($f,$str_payload);
+fclose($f);
+
+$result = file_get_contents(QN_BASEDIR."/packages/{$package}/uml/{$path}/{$filename}");
+
+$context->httpResponse()
+ ->body($result)
+ ->status($response_code)
+ ->send();
+
+function endsWith( $haystack, $needle ) {
+ $length = strlen( $needle );
+ if( !$length ) {
+ return true;
+ }
+ return substr( $haystack, -$length ) === $needle;
+}
\ No newline at end of file
diff --git a/packages/core/actions/config/update-workflow.php b/packages/core/actions/config/update-workflow.php
new file mode 100644
index 000000000..82dec5fa3
--- /dev/null
+++ b/packages/core/actions/config/update-workflow.php
@@ -0,0 +1,122 @@
+
+ Some Rights Reserved, Cedric Francoys, 2010-2021
+ Licensed under GNU LGPL 3 license
+*/
+use PhpParser\{Node, NodeTraverser, NodeVisitorAbstract, ParserFactory, NodeFinder};
+
+list($params, $providers) = eQual::announce([
+ 'description' => "Translate a workflow definition of a given entity to a PHP method and store it in related file.",
+ 'help' => "This controller rely on the PHP binary. In order to make them work, sure the PHP binary is present in the PATH.",
+ 'response' => [
+ 'content-type' => 'text/plain',
+ 'charset' => 'UTF-8',
+ 'accept-origin' => '*'
+ ],
+ 'params' => [
+ 'entity' => [
+ 'description' => 'Name of the entity (class).',
+ 'type' => 'string',
+ 'required' => true
+ ],
+ 'payload' => [
+ 'description' => 'Workflow definition.',
+ 'type' => 'array',
+ 'required' => true
+ ]
+ ],
+ 'providers' => ['context', 'orm']
+]);
+
+/**
+ * @var \equal\php\Context $context
+ * @var \equal\orm\ObjectManager $orm
+ */
+list($context, $orm) = [$providers['context'], $providers['orm']];
+
+// Create all the object to use for using PhpParser
+$parser = (new ParserFactory)->create(ParserFactory::PREFER_PHP7);
+$nodeFinder = new NodeFinder;
+$traverser = new NodeTraverser;
+$prettyPrinter = new PhpParser\PrettyPrinter\Standard;
+// Get the parts of the entity string, separated by backslashes
+$parts = explode('\\', $params['entity']);
+// Get the package name from the first part of the string
+$package = array_shift($parts);// Get the package name from the first part of the string
+// Get the file name from the last part of the string
+$filename = array_pop($parts);
+// Get the class path from the remaining part
+$class_path = implode('/', $parts);
+
+$getColumns = null;
+$code_php = null;
+
+// Decode the JSON string to a PHP object
+$code_php = $params['payload'];
+// Get a string representation from the code_php variable, with backslashes escaped
+$code_string = str_replace("\\\\", "\\", var_export($code_php, true));
+// Create a virtual file holing the default structure with the parsed code and generate a minimal AST
+$ast_temp = $parser->parse("findFirst(
+ $ast_temp,
+ function(Node $node) {
+ return isset($node->name->name)
+ && $node->name->name === "getWorkflow";
+ }
+);
+
+// Add a visitor hijack the getWorkflow method and update its content with new schema
+$traverser->addVisitor(
+ new class($nodeGetWorkflow, "getWorkflow") extends NodeVisitorAbstract {
+ private $node;
+ private $target;
+ public function __construct($node, $target) {
+ $this->node = $node;
+ $this->target = $target;
+ }
+ public function leaveNode(Node $node) {
+ if (
+ isset($node->name->name)
+ && $node->name->name === $this->target
+ ) {
+ return $this->node;
+ }
+
+ return null;
+ }
+ }
+ );
+
+// Get the full path of the file
+$file = QN_BASEDIR."/packages/{$package}/classes/{$class_path}/{$filename}.class.php";
+// Get the code from the original file ...
+$code = file_get_contents($file);
+// ... and parse it to create an AST
+$stmtOriginal = $parser->parse($code);
+// Update the AST by using visitors attached to the traverser
+$stmtModified = $traverser->traverse($stmtOriginal);
+// Pretty print the modified AST ...
+$result = $prettyPrinter->prettyPrintFile($stmtModified);
+// ... and write back the code to the file
+if(file_put_contents($file, $result) === false) {
+ throw new Exception('io_error', QN_ERROR_UNKNOWN);
+}
+
+try {
+ // apply coding standards (ecs.php is expected in QN_BASEDIR)
+ $command = 'php ./vendor/bin/ecs check "' . str_replace('\\', '/', $file) . '" --fix';
+ if(exec($command) === false) {
+ throw new Exception('command_failed', QN_ERROR_UNKNOWN);
+ }
+ $result = file_get_contents($file);
+}
+catch(Exception $e) {
+ trigger_error("PHP::unable to beautify rendered file ($file): ".$e->getMessage(), QN_REPORT_INFO);
+}
+
+$context->httpResponse()
+ ->status(204)
+ ->body($result)
+ ->send();
diff --git a/packages/core/actions/cron/run.php b/packages/core/actions/cron/run.php
index 205703864..91268117c 100644
--- a/packages/core/actions/cron/run.php
+++ b/packages/core/actions/cron/run.php
@@ -35,7 +35,7 @@
*/
list($context, $cron, $am) = [$providers['context'], $providers['cron'], $providers['access']];
-if(!$am->hasGroup('admin')) {
+if(!$am->hasGroup('admins')) {
throw new Exception('admin_only', QN_ERROR_NOT_ALLOWED);
}
diff --git a/packages/core/actions/group/grant.php b/packages/core/actions/group/grant.php
index d6b526a65..dc6b87250 100644
--- a/packages/core/actions/group/grant.php
+++ b/packages/core/actions/group/grant.php
@@ -48,7 +48,7 @@
'manage' => QN_R_MANAGE
];
-if(!$ac->isAllowed(QN_R_MANAGE, $operation, $params['entity'])) {
+if(!$ac->isAllowed(QN_R_MANAGE, $params['entity'])) {
throw new \Exception('MANAGE,'.$params['entity'], QN_ERROR_NOT_ALLOWED);
}
diff --git a/packages/core/actions/group/revoke.php b/packages/core/actions/group/revoke.php
index 47fcc5e0e..d2577dfd0 100644
--- a/packages/core/actions/group/revoke.php
+++ b/packages/core/actions/group/revoke.php
@@ -48,7 +48,7 @@
'manage' => QN_R_MANAGE
];
-if(!$ac->isAllowed(QN_R_MANAGE, $operation, $params['entity'])) {
+if(!$ac->isAllowed(QN_R_MANAGE, $params['entity'])) {
throw new \Exception('MANAGE,'.$params['entity'], QN_ERROR_NOT_ALLOWED);
}
diff --git a/packages/core/actions/init.php b/packages/core/actions/init.php
new file mode 100644
index 000000000..085f79d64
--- /dev/null
+++ b/packages/core/actions/init.php
@@ -0,0 +1,39 @@
+
+ Some Rights Reserved, Cedric Francoys, 2010-2021
+ Licensed under GNU LGPL 3 license
+*/
+
+list($params, $providers) = eQual::announce([
+ 'description' => 'Create a database according to the configuration',
+ 'params' => [],
+ 'providers' => ['context', 'orm'],
+ 'constants' => ['ENV_MODE', 'ROOT_APP_URL', 'APP_NAME', 'APP_LOGO_URL', 'DEFAULT_LANG', 'L10N_LOCALE', 'EQ_VERSION', 'ORG_NAME', 'ORG_URL'],
+]);
+
+list($context, $orm) = [$providers['context'], $providers['orm']];
+
+// init database
+eQual::run('do', 'init_db');
+
+// export config to public folder
+$config = [
+ 'production' => constant('ENV_MODE'),
+ 'parent_domain' => parse_url(constant('ROOT_APP_URL'), PHP_URL_HOST),
+ 'backend_url' => constant('ROOT_APP_URL'),
+ 'rest_api_url' => constant('ROOT_APP_URL').'/',
+ 'lang' => constant('DEFAULT_LANG'),
+ 'locale' => constant('L10N_LOCALE'),
+ 'version' => constant('EQ_VERSION'),
+ 'company_name' => constant('ORG_NAME'),
+ 'company_url' => constant('ORG_URL'),
+ 'app_name' => constant('APP_NAME'),
+ 'app_logo_url' => constant('APP_LOGO_URL')
+];
+
+$json = json_encode($config, JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES|JSON_UNESCAPED_UNICODE);
+file_put_contents(QN_BASEDIR.'/public/assets/env/config.json', $json);
+
+
+// create mandatory files
diff --git a/packages/core/actions/init/db.php b/packages/core/actions/init/db.php
index d5b561735..5c7dd9a5a 100644
--- a/packages/core/actions/init/db.php
+++ b/packages/core/actions/init/db.php
@@ -7,7 +7,7 @@
use equal\db\DBConnection;
-list($params, $providers) = announce([
+list($params, $providers) = eQual::announce([
'description' => 'Create a database according to the configuration',
'params' => [],
'providers' => ['context', 'orm'],
@@ -16,14 +16,7 @@
list($context, $orm) = [$providers['context'], $providers['orm']];
-$json = run('do', 'test_db-connectivity');
-
-if(strlen($json)) {
- // relay result
- print($json);
- // return an error code
- exit(1);
-}
+eQual::run('do', 'test_db-connectivity');
// create Master database
$db = DBConnection::getInstance(constant('DB_HOST'), constant('DB_PORT'), constant('DB_NAME'), constant('DB_USER'), constant('DB_PASSWORD'), constant('DB_DBMS'))->connect(false);
diff --git a/packages/core/actions/init/package.php b/packages/core/actions/init/package.php
index 2efb88248..dbe746aeb 100644
--- a/packages/core/actions/init/package.php
+++ b/packages/core/actions/init/package.php
@@ -129,7 +129,7 @@
foreach (glob($data_folder."/*.json") as $json_file) {
$data = file_get_contents($json_file);
$classes = json_decode($data, true);
- foreach($classes as $class){
+ foreach($classes as $class) {
$entity = $class['name'];
$lang = $class['lang'];
$model = $orm->getModel($entity);
@@ -142,21 +142,23 @@
$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
- $id = $odata['id'];
- $res = $orm->update($entity, $id, $odata, $lang);
- $objects_ids[] = $id;
}
else {
- $objects_ids[] = $orm->create($entity, $odata, $lang);
+ $orm->create($entity, ['id' => $odata['id']], $lang, false);
}
+ $id = $odata['id'];
+ unset($odata['id']);
}
else {
- $objects_ids[] = $orm->create($entity, $odata, $lang);
+ $id = $orm->create($entity, [], $lang);
}
+ $orm->update($entity, $id, $odata, $lang);
+ $objects_ids[] = $id;
}
// force a first generation of computed fields, if any
diff --git a/packages/core/actions/model/onchange.php b/packages/core/actions/model/onchange.php
index ec3220057..98d3193c5 100644
--- a/packages/core/actions/model/onchange.php
+++ b/packages/core/actions/model/onchange.php
@@ -7,7 +7,7 @@
use equal\orm\Field;
use equal\orm\Collections;
-list($params, $providers) = announce([
+list($params, $providers) = eQual::announce([
'description' => "Transform an object being edited in a view, according to the onchange method of the entity, if any.",
'response' => [
'content-type' => 'application/json',
@@ -101,7 +101,7 @@
if ($data !== false) {
$msg = $data;
}
- throw new \Exception(serialize([$field => $msg]), $e->getCode());
+ throw new Exception(serialize([$field => $msg]), $e->getCode());
}
}
@@ -145,13 +145,19 @@
continue;
}
// convert objects to arrays (for supporting values retrieved as sub-objects)
- if(is_subclass_of($value, 'equal\orm\Model')) {
- $result[$field] = $value->toArray();
- continue;
+ if(is_object($value)) {
+ if(is_subclass_of($value, 'equal\orm\Model')) {
+ $result[$field] = $value->toArray();
+ }
+ else {
+ $result[$field] = serialize($value);
+ }
+ }
+ else {
+ $f = new Field($schema[$field]);
+ // adapt received values based on their type (as defined in schema)
+ $result[$field] = $adapter->adaptOut($value, $f->getUsage());
}
- $f = new Field($schema[$field]);
- // adapt received values based on their type (as defined in schema)
- $result[$field] = $adapter->adaptOut($value, $f->getUsage());
}
}
diff --git a/packages/core/actions/spool/run.php b/packages/core/actions/spool/run.php
index 5627ba824..86af5ecdd 100644
--- a/packages/core/actions/spool/run.php
+++ b/packages/core/actions/spool/run.php
@@ -18,7 +18,7 @@
]);
-// initalise local vars with inputs
+// initialize local vars with inputs
list($om, $context, $auth) = [ $providers['orm'], $providers['context'], $providers['auth'] ];
Mail::flush();
diff --git a/packages/core/actions/test/db-connectivity.php b/packages/core/actions/test/db-connectivity.php
index a9df16c84..c96e0dea9 100644
--- a/packages/core/actions/test/db-connectivity.php
+++ b/packages/core/actions/test/db-connectivity.php
@@ -6,7 +6,7 @@
*/
use equal\db\DBConnection;
-$params = announce([
+$params = eQual::announce([
'description' => "Tests connectivity to the DBMS server.\nIn case of success, the script simply terminates with a status code of 0 (no output)",
'params' => [],
'constants' => ['DB_HOST', 'DB_PORT', 'DB_NAME', 'DB_USER', 'DB_PASSWORD', 'DB_DBMS']
@@ -17,12 +17,12 @@
// 1) test access to DBMS service
if(!$db->canConnect()) {
- throw new Exception('Unable to establish connection to DBMS host (wrong hostname or port).', QN_ERROR_INVALID_CONFIG);
+ throw new Exception('Unable to establish connection to DBMS host (wrong hostname).', QN_ERROR_INVALID_CONFIG);
}
// 2) try to connect to DBMS
if(!$db->connected()) {
if($db->connect(false) == false) {
- throw new Exception('Unable to establish connection to DBMS host (wrong credentials).', QN_ERROR_INVALID_CONFIG);
+ throw new Exception('Unable to establish connection to DBMS host (wrong port or wrong credentials).', QN_ERROR_INVALID_CONFIG);
}
}
// 3) everything went well: disconnect
diff --git a/packages/core/actions/test/fs-consistency.php b/packages/core/actions/test/fs-consistency.php
index 2b64a0fc3..89f367786 100644
--- a/packages/core/actions/test/fs-consistency.php
+++ b/packages/core/actions/test/fs-consistency.php
@@ -4,10 +4,10 @@
Some Rights Reserved, Cedric Francoys, 2010-2021
Licensed under GNU GPL 3 license
*/
-$params = announce([
+$params = eQual::announce([
'description' => 'Checks current installation directories integrity',
'params' => [],
- 'constants' => ['FILE_STORAGE_MODE', 'ROUTING_METHOD']
+ 'constants' => ['FILE_STORAGE_MODE', 'ROUTING_METHOD', 'HTTP_PROCESS_USERNAME']
]);
// array holding files and directories to be tested
@@ -28,6 +28,10 @@
'rights' => QN_R_READ | QN_R_WRITE,
'path' => QN_BASEDIR.'/spool'
],
+ [
+ 'rights' => QN_R_READ,
+ 'path' => QN_BASEDIR.'/lib'
+ ],
[
'rights' => QN_R_READ,
'path' => QN_BASEDIR.'/config'
@@ -94,17 +98,24 @@ function check_permissions($path, $mask, $uid=0) {
$uid = 0;
-// #todo - add HTTP_PROCESS_USERNAME
-$username = 'www-data';
+
+$username = constant('HTTP_PROCESS_USERNAME');
+
// get UID of a use by its name
-if(exec("id -u \"$username\"", $output)) {
+exec("id -u \"$username\" 2>&1", $output);
+
+if(count($output)) {
$uid = intval(reset($output));
}
+if(!$uid) {
+ throw new Exception(serialize(['unknown_user' => $username]), QN_ERROR_INVALID_CONFIG);
+}
+
// check mod
foreach($paths as $item) {
if(!file_exists($item['path'])) {
- throw new Exception("Missing mandatory node {$item['path']}", QN_ERROR_INVALID_CONFIG);
+ throw new Exception(serialize(['missing_mandatory_node' => $item['path']]), QN_ERROR_INVALID_CONFIG);
}
if( ($res = check_permissions($item['path'], $item['rights'], $uid)) <= 0) {
switch(-$res) {
diff --git a/packages/core/actions/test/package-consistency.php b/packages/core/actions/test/package-consistency.php
index 6d94c6086..ed604796c 100644
--- a/packages/core/actions/test/package-consistency.php
+++ b/packages/core/actions/test/package-consistency.php
@@ -225,7 +225,7 @@
]
];
}
- else if(strpos($view_file, 'list.') > 0) {
+ elseif(strpos($view_file, 'list.') > 0) {
$structure = [
'name',
'description',
@@ -236,7 +236,7 @@
]
];
}
- else if(strpos($view_file, 'chart.') > 0) {
+ elseif(strpos($view_file, 'chart.') > 0) {
$structure = [
'name',
'description',
@@ -319,12 +319,12 @@
$result[] = "WARN - I18 - Value for property '$property' should not end by '.' for field '$field' referenced in file $i18n_file";
}
}
- else if($property == 'help') {
+ elseif($property == 'help') {
if( mb_strlen($data['model'][$field][$property]) && !in_array(substr($data['model'][$field][$property], -1), ['.', '?', '!']) ) {
$result[] = "WARN - I18 - Value for property '$property' should end by '.' for field '$field' referenced in file $i18n_file";
}
}
- else if($property == 'description') {
+ elseif($property == 'description') {
if( mb_strlen($data['model'][$field][$property]) ) {
if( !in_array(substr($data['model'][$field][$property], -1), ['.', '?', '!']) ) {
$result[] = "WARN - I18 - Value for property '$property' should end by '.' for field '$field' referenced in file $i18n_file";
@@ -437,7 +437,7 @@
if(isset($descriptor['onupdate'])) {
$parts = explode('::', $descriptor['onupdate']);
- $count = count($parts);
+ $count = count((array) $parts);
$called_class = $entity;
$called_method = $descriptor['onupdate'];
@@ -550,7 +550,7 @@ function view_test($data, $structure) {
return "missing property '$key' for item $index";
}
}
- else if(isset($elem[$key]) && isset($structure[$item])){
+ elseif(isset($elem[$key]) && isset($structure[$item])){
$res = view_test($elem[$key], $structure[$item]);
if($res) {
return $res;
diff --git a/packages/core/actions/test/php-extensions.php b/packages/core/actions/test/php-extensions.php
new file mode 100644
index 000000000..e9335f1b0
--- /dev/null
+++ b/packages/core/actions/test/php-extensions.php
@@ -0,0 +1,45 @@
+
+ Some Rights Reserved, Cedric Francoys, 2010-2021
+ Licensed under GNU GPL 3 license
+*/
+$params = eQual::announce([
+ 'description' => 'Checks current installation PHP loaded extensions.',
+ 'help' => "This controller raises an exception if one or more mandatory PHP extensions are not loaded in the current environment.\n
+ If the `dbms` param is set to True, then it will also raise an exception if none of the supported DBMS are supported through a specific PHP extension.",
+ 'params' => [
+ 'dbms' => [
+ 'type' => 'boolean',
+ 'description' => 'Flag for testing support for at least one DBMS.',
+ 'default' => false
+ ]
+ ]
+]);
+
+if(!is_callable('get_loaded_extensions')) {
+ throw new Exception("missing_dependency", QN_ERROR_INVALID_CONFIG);
+}
+
+$extensions = get_loaded_extensions();
+
+// mandatory PHP extensions
+$ext_mandatory = ['gd', 'intl', 'iconv','json', 'libxml', 'mbstring', 'SimpleXML', 'zip'];
+
+// supported DBMS extensions
+$ext_dbms = ['sqlite3', 'mysqli', 'sqlsrv'];
+
+
+$diff_mandatory = array_diff($ext_mandatory, $extensions);
+if(count($diff_mandatory)) {
+ throw new Exception(serialize(['missing_extension' => $diff_mandatory]), QN_ERROR_INVALID_CONFIG);
+}
+
+if($params['dbms']) {
+ $diff_dbms = array_diff($ext_dbms, $extensions);
+ if(count($diff_dbms) >= count($ext_dbms)) {
+ throw new Exception(serialize(['missing_one_amongst' => $diff_dbms]), QN_ERROR_INVALID_CONFIG);
+ }
+}
+
+// no error, exit code will be 0
\ No newline at end of file
diff --git a/packages/core/actions/user/grant.php b/packages/core/actions/user/grant.php
index 013c37d71..6fa5c9bd1 100644
--- a/packages/core/actions/user/grant.php
+++ b/packages/core/actions/user/grant.php
@@ -55,7 +55,7 @@
];
if(!$ac->isAllowed(QN_R_MANAGE, $params['entity'])) {
- throw new \Exception('MANAGE,'.$params['entity'], QN_ERROR_NOT_ALLOWED);
+ throw new Exception('MANAGE,'.$params['entity'], QN_ERROR_NOT_ALLOWED);
}
// 1) retrieve targeted user
@@ -65,7 +65,7 @@
$ids = User::search(['id', '=', $user_id])->ids();
if(!count($ids)) {
- throw new \Exception("unknown_user_id", QN_ERROR_UNKNOWN_OBJECT);
+ throw new Exception("unknown_user_id", QN_ERROR_UNKNOWN_OBJECT);
}
}
else {
@@ -73,7 +73,7 @@
$ids = User::search(['login', '=', $params['user']])->ids();
if(!count($ids)) {
- throw new \Exception("unknown_username", QN_ERROR_UNKNOWN_OBJECT);
+ throw new Exception("unknown_username", QN_ERROR_UNKNOWN_OBJECT);
}
$user_id = array_shift($ids);
@@ -85,7 +85,7 @@
$acl_ids = $orm->search('core\Permission', [ ['class_name', '=', $params['entity']], ['user_id', '=', $user_id] ]);
if($acl_ids < 0 || !count($acl_ids)) {
- throw new \Exception("acl_creation_failed", QN_ERROR_UNKNOWN);
+ throw new Exception("acl_creation_failed", QN_ERROR_UNKNOWN);
}
$acls = $orm->read('core\Permission', $acl_ids, ['user_id', 'class_name', 'rights', 'rights_txt']);
diff --git a/packages/core/actions/user/revoke.php b/packages/core/actions/user/revoke.php
index 4822455a6..65a471ec4 100644
--- a/packages/core/actions/user/revoke.php
+++ b/packages/core/actions/user/revoke.php
@@ -45,8 +45,8 @@
'manage' => QN_R_MANAGE
];
-if(!$ac->isAllowed(QN_R_MANAGE, $operation, $params['entity'])) {
- throw new \Exception('MANAGE,'.$params['entity'], QN_ERROR_NOT_ALLOWED);
+if(!$ac->isAllowed(QN_R_MANAGE, $params['entity'])) {
+ throw new Exception('MANAGE,'.$params['entity'], QN_ERROR_NOT_ALLOWED);
}
// retrieve targeted user
@@ -56,7 +56,7 @@
$ids = User::search(['id', '=', $user_id])->ids();
if(!count($ids)) {
- throw new \Exception("unknown_user_id", QN_ERROR_UNKNOWN_OBJECT);
+ throw new Exception("unknown_user_id", QN_ERROR_UNKNOWN_OBJECT);
}
}
else {
@@ -64,7 +64,7 @@
$ids = User::search(['login', '=', $params['user']])->ids();
if(!count($ids)) {
- throw new \Exception("unknown_username", QN_ERROR_UNKNOWN_OBJECT);
+ throw new Exception("unknown_username", QN_ERROR_UNKNOWN_OBJECT);
}
$user_id = array_shift($ids);
diff --git a/packages/core/actions/user/signin.php b/packages/core/actions/user/signin.php
index f9c511dec..16ac69ae1 100644
--- a/packages/core/actions/user/signin.php
+++ b/packages/core/actions/user/signin.php
@@ -7,7 +7,7 @@
use core\User;
// announce script and fetch parameters values
-list($params, $providers) = announce([
+list($params, $providers) = eQual::announce([
'description' => "Attempts to log a user in.",
'params' => [
'login' => [
@@ -27,7 +27,7 @@
'accept-origin' => '*'
],
'providers' => ['context', 'auth', 'orm'],
- 'constants' => ['AUTH_ACCESS_TOKEN_VALIDITY', 'AUTH_REFRESH_TOKEN_VALIDITY', 'AUTH_TOKEN_HTTPS']
+ 'constants' => ['ROOT_APP_URL', 'AUTH_ACCESS_TOKEN_VALIDITY', 'AUTH_REFRESH_TOKEN_VALIDITY', 'AUTH_TOKEN_HTTPS']
]);
/**
@@ -77,7 +77,8 @@
->cookie('access_token', $access_token, [
'expires' => time() + constant('AUTH_ACCESS_TOKEN_VALIDITY'),
'httponly' => true,
- 'secure' => constant('AUTH_TOKEN_HTTPS')
+ 'secure' => constant('AUTH_TOKEN_HTTPS'),
+ 'domain' => parse_url(constant('ROOT_APP_URL'), PHP_URL_HOST)
])
->status(204)
->send();
diff --git a/packages/core/actions/user/signout.php b/packages/core/actions/user/signout.php
index e07ea0b13..2e90a2f71 100644
--- a/packages/core/actions/user/signout.php
+++ b/packages/core/actions/user/signout.php
@@ -6,11 +6,11 @@
*/
// announce script and fetch parameters values
-list($params, $providers) = announce([
+list($params, $providers) = eQual::announce([
'description' => "Sign a user out.",
'params' => [
],
- 'constants' => ['AUTH_TOKEN_HTTPS'],
+ 'constants' => ['ROOT_APP_URL', 'AUTH_TOKEN_HTTPS'],
'response' => [
'content-type' => 'application/json',
'charset' => 'utf-8',
@@ -20,6 +20,11 @@
$context->httpResponse()
- ->cookie('access_token', '', ['expires' => time(), 'httponly' => true, 'secure' => constant('AUTH_TOKEN_HTTPS')])
+ ->cookie('access_token', '', [
+ 'expires' => time(),
+ 'httponly' => true,
+ 'secure' => constant('AUTH_TOKEN_HTTPS'),
+ 'domain' => parse_url(constant('ROOT_APP_URL'), PHP_URL_HOST)
+ ])
->status(204)
- ->send();
\ No newline at end of file
+ ->send();
diff --git a/packages/core/apps/apps/version b/packages/core/apps/apps/version
new file mode 100644
index 000000000..3bf35772e
--- /dev/null
+++ b/packages/core/apps/apps/version
@@ -0,0 +1 @@
+7227b9c566adbfcdfe5e5d12ec1925b0
diff --git a/packages/core/apps/apps/web.app b/packages/core/apps/apps/web.app
index 1aa7fcd6a..0eff529ba 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 34fadaec5..2ff31b6b0 100644
--- a/packages/core/apps/auth/version
+++ b/packages/core/apps/auth/version
@@ -1 +1 @@
-3a970b6cc58918da4c55587256c3f35d
+a382468b6c29901e622b3e275916bd91
diff --git a/packages/core/apps/auth/web.app b/packages/core/apps/auth/web.app
index e6935acb0..29b17d6e4 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
new file mode 100644
index 000000000..dfa950939
--- /dev/null
+++ b/packages/core/apps/console.php
@@ -0,0 +1,228 @@
+
+ Some Rights Reserved, Cedric Francoys, 2010-2021
+ Licensed under GNU LGPL 3 license
+*/
+
+list($params, $providers) = eQual::announce([
+ 'description' => 'Returns a descriptor of current installation Settings, holding specific values for current User, if applicable.',
+ 'access' => [
+ 'visibility' => 'private'
+ ],
+ 'params' => [
+ 'thread_id' => [
+ 'type' => 'string',
+ 'description' => 'Thread identifier of the line (8 hex chars).'
+ ],
+ 'level' => [
+ 'type' => 'string',
+ 'description' => 'Level of the threads to display.',
+ 'selection' => [
+ 'debug',
+ 'info',
+ 'warning',
+ 'error'
+ ]
+ ],
+ 'mode' => [
+ 'type' => 'string',
+ 'description' => 'Mode of the threads to display.',
+ 'selection' => [
+ 'php',
+ 'orm',
+ 'sql',
+ 'api',
+ 'app'
+ ]
+ ],
+ 'date' => [
+ 'type' => 'date',
+ 'description' => 'Date (time) of the threads to display.'
+ ],
+ 'limit' => [
+ 'type' => 'integer',
+ 'description' => 'Limit of the number of lines to return.'
+ ]
+ ],
+ 'response' => [
+ 'content-type' => 'text/plain',
+ 'charset' => 'UTF-8',
+ 'accept-origin' => '*'
+ ],
+ 'providers' => ['context']
+]);
+
+/**
+ * @var \equal\php\Context $context
+ */
+list($context) = [$providers['context']];
+
+$style_red = function($text) {return "\e[31;1m".$text."\e[0m";};
+$style_green = function($text) {return "\e[32;1m".$text."\e[0m";};
+$style_blue = function($text) {return "\e[34;1m".$text."\e[0m";};
+$style_yellow = function($text) {return "\e[33;1m".$text."\e[0m";};
+$style_white = function($text) {return "\e[0m".$text."\e[0m";};
+$style_bold = function($text) {return "\e[1m".$text."\e[0m";};
+$style_italic = function($text) {return "\e[3m".$text."\e[0m";};
+$style_underline = function($text) {return "\e[4m".$text."\e[0m";};
+
+/**
+ * @var string $level
+ * @return string ANSI escape codes to change the color of the level text according to their values
+ * example $level = "WARNING" returns yellow color
+ */
+$style_level = function (string $level) use($style_red, $style_green, $style_blue, $style_yellow, $style_white, $style_bold, $style_italic, $style_underline) {
+ if(!is_null($level)) {
+ switch(strtoupper($level)) {
+ case 'WARNING':
+ case E_USER_WARNING:
+ return $style_yellow($level);
+ case 'DEBUG':
+ case E_USER_DEPRECATED:
+ return $style_green($level);
+ case 'INFO':
+ case 'NOTICE':
+ case E_USER_NOTICE:
+ return $style_blue($level);
+ case 'ERROR':
+ case 'FATAL':
+ case 'Fatal error':
+ case 'Parse error':
+ return $style_red($level);
+ default:
+ return $style_white($level);
+ }
+ }
+ return $style_white($level);
+};
+
+/**
+ * Displays a thread
+ * @var Array $thread
+ */
+$thread_display = function (array $thread) use($style_red, $style_green, $style_blue, $style_yellow, $style_white, $style_bold, $style_italic, $style_underline, $style_level) {
+
+ $text = "";
+
+ $info = array_merge([
+ 'thread_id' => '',
+ 'time' => '',
+ 'mtime' => '',
+ 'level' => '',
+ 'mode' => '',
+ 'function' => '',
+ 'file' => '',
+ 'line' => '',
+ 'message' => '',
+ 'stack' => []
+ ], $thread);
+
+ $text .= $style_red($info['thread_id']).' ';
+ $text .= $style_bold($info['time']).' ';
+ $text .= $style_bold($info['mtime']).' ';
+ $text .= $style_level($info['level']).' ';
+ $text .= $info['mode'].' ';
+ $text .= $style_underline($info['function']).'() ';
+ $text .= "@ {$info['file']} : ";
+ $text .= 'line '.$style_bold($info['line']).' ';
+
+ $text .= PHP_EOL;
+
+ if(strlen($info['message'])) {
+ // check message format to display in lines if it is an associative array
+ $message = json_decode($info['message'], true);
+ if(is_array($message)) {
+ foreach($message as $value) {
+ if(is_array($value)) {
+ $m = "";
+ foreach($value as $id => $val) {
+ if(is_array($val)) {
+ $val = implode(',', $val);
+ }
+ $m .= $style_bold($id).' : '.$style_italic($val).PHP_EOL;
+ }
+ $text .= "$m".PHP_EOL;
+ }
+ elseif(is_string($value)) {
+ // message displays in italics
+ $text .= $style_italic($value).PHP_EOL;
+ }
+ }
+ }
+ else {
+ // message displays in italics
+ $text .= $style_italic($info['message']);
+ }
+ }
+ $stack = (array) $info['stack'];
+ if(count($stack)) {
+ for($i = 0, $n = count($stack); $i < $n; $i++) {
+ $index = $n - $i - 1;
+ $function = (isset($stack[$index]['function']))?$stack[$index]['function']:'';
+ $file = (isset($stack[$index]['file']))?$stack[$index]['file']:'';
+ $line = (isset($stack[$index]['line']))?$stack[$index]['line']:'';
+ $text .= PHP_EOL.($i == ($n - 1))?' └ ':' ├ ';
+ $text .= "{$function} @ {$file} {$line} ";
+ }
+ }
+ return $text;
+};
+
+/**
+ * Filters a thread arguments are given in params
+ * @return Array $thread | null
+ */
+$thread_filter = function (array $thread, array $params) {
+ if(isset($params['mode']) && $params['mode'] !== '') {
+ return (strcasecmp($thread['mode'], $params['mode']) == 0);
+ }
+ if(isset($params['level']) && isset($params['level']) != '') {
+ return (strcasecmp($thread['level'], $params['level']) == 0);
+ }
+ if(isset($params['thread_id']) && $params['thread_id'] != '') {
+ return (strpos($thread['thread_id'], $params['thread_id']) === 0);
+ }
+ if(isset($params['date'])) {
+ $delta = strtotime($thread['time']) - intval($params['date']);
+ return ($delta >= 0 && $delta <= 86400);
+ }
+ return true;
+};
+
+$result = [];
+
+if(file_exists(QN_BASEDIR.'/log/eq_error.log')) {
+ // read raw data from pointer log file
+ $fp = fopen(QN_BASEDIR.'/log/eq_error.log', "r");
+ $result[] = "START LOG";
+ $i = 0;
+ $prev_thread_id = 0;
+ if($fp) {
+ while((($data = stream_get_line($fp, 65535, PHP_EOL)) !== false)) {
+ if(isset($params["limit"]) && $i > $params["limit"]) {
+ break;
+ }
+ $thread = json_decode($data, true);
+ if(!is_null($thread) && $thread_filter($thread, $params)) {
+ if($prev_thread_id) {
+ if($thread['thread_id'] != $prev_thread_id) {
+ $result[] = "===========================================================================================================================================";
+ }
+ else {
+ $result[] = "-------------------------------------------------------------------------------------------------------------------------------------------";
+ }
+ }
+ $result[] = $thread_display($thread);
+ $prev_thread_id = $thread['thread_id'];
+ $i++;
+ }
+ }
+ fclose($fp);
+ }
+ $result[] = PHP_EOL."END LOG \e[0m";
+}
+
+$context->httpResponse()
+ ->body(implode(PHP_EOL, $result))
+ ->send();
diff --git a/packages/core/apps/settings/manifest.json b/packages/core/apps/settings/manifest.json
index 9a76fa92e..b4ace588e 100644
--- a/packages/core/apps/settings/manifest.json
+++ b/packages/core/apps/settings/manifest.json
@@ -10,7 +10,7 @@
"color": "#FF9741",
"access": {
"groups": [
- "admin"
+ "admins"
]
},
"show_in_apps": true
diff --git a/packages/core/apps/settings/version b/packages/core/apps/settings/version
index fcc949118..1e00613cf 100644
--- a/packages/core/apps/settings/version
+++ b/packages/core/apps/settings/version
@@ -1 +1 @@
-8185b469441de68537124e6d10531c7b
+a6a5979cc76c971fdbfde6cfa6ff8544
diff --git a/packages/core/apps/settings/web.app b/packages/core/apps/settings/web.app
index 9d90459fa..fbf14585c 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/workbench/.gitignore b/packages/core/apps/workbench/.gitignore
deleted file mode 100644
index eef52883d..000000000
--- a/packages/core/apps/workbench/.gitignore
+++ /dev/null
@@ -1 +0,0 @@
-source/
diff --git a/packages/core/apps/workbench/manifest.json b/packages/core/apps/workbench/manifest.json
old mode 100644
new mode 100755
index 73da546c0..d661e9edd
--- a/packages/core/apps/workbench/manifest.json
+++ b/packages/core/apps/workbench/manifest.json
@@ -1,16 +1,12 @@
{
"name": "Workbench",
- "description": "App for customizing models, views and controllers from the user interface without code.",
- "version": "1.0",
- "authors": ["Cedric Francoys", "Sylvain Sausse", "Quentin Leveque"],
- "license": "LGPL-3",
- "repository": "https://github.com/equalframework/apps-core-settings.git",
+ "description": "This is the workbench application.",
"url": "/workbench",
"icon": "edit_note",
- "color": "#0f1397",
+ "color": "#d2252c",
"access": {
"groups": [
- "users", "admin"
+ "users", "admins"
]
},
"show_in_apps": true
diff --git a/packages/core/apps/workbench/source b/packages/core/apps/workbench/source
new file mode 160000
index 000000000..189b60ebc
--- /dev/null
+++ b/packages/core/apps/workbench/source
@@ -0,0 +1 @@
+Subproject commit 189b60ebc908cc21647a243055045fb78a0defb0
diff --git a/packages/core/apps/workbench/version b/packages/core/apps/workbench/version
new file mode 100644
index 000000000..ba0cfa371
--- /dev/null
+++ b/packages/core/apps/workbench/version
@@ -0,0 +1 @@
+5c35e5d9b0686cd9cd0b1ef66a6461e2
diff --git a/packages/core/apps/workbench/web.app b/packages/core/apps/workbench/web.app
index be2b6f8fc..3a2d0c095 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 fab6fce6c..1467ca737 100644
--- a/packages/core/classes/Mail.class.php
+++ b/packages/core/classes/Mail.class.php
@@ -240,10 +240,20 @@ public static function flush() {
$i = 0;
// loop through messages
foreach($queue as $file => $message) {
- // prevent handling handling more than $max messages (successfully sent)
+ // prevent handling more than $max messages (successfully sent)
if($i > $max) {
break;
}
+
+ if(isset($message['id'])) {
+ $mailMessage = self::id($message['id'])->read(['status'])->first();
+ // prevent re-sending already sent messages
+ if($mailMessage['status'] == 'sent') {
+ unlink(self::MESSAGE_FOLDER.'/'.$file);
+ continue;
+ }
+ }
+
$body = (isset($message['body']))?$message['body']:'';
$subject = (isset($message['subject']))?$message['subject']:'';
diff --git a/packages/core/classes/Meta.class.php b/packages/core/classes/Meta.class.php
new file mode 100644
index 000000000..d0aafd422
--- /dev/null
+++ b/packages/core/classes/Meta.class.php
@@ -0,0 +1,57 @@
+
+ Some Rights Reserved, Cedric Francoys, 2010-2021
+ Licensed under GNU GPL 3 license
+*/
+namespace core;
+
+use equal\orm\Model;
+
+class Meta extends Model {
+
+ public static function getDescription() {
+ return "Meta values are used to store various additional information related to back-end or front-end components.";
+ }
+
+ public static function getColumns() {
+ return [
+ 'name' => [
+ 'type' => 'computed',
+ 'result_type' => 'string',
+ 'description' => 'Name of the meta (code+reference).',
+ 'function' => 'calcName',
+ 'store' => true
+ ],
+
+ 'code' => [
+ 'type' => 'string',
+ 'description' => 'Identifier of the meta (arbitrary: \'workflow\', \'uml\', \'pipeline\', ...).',
+ 'dependencies' => ['name']
+ ],
+
+ 'reference' => [
+ 'type' => 'string',
+ 'description' => 'Custom reference (arbitrary: object_class, object_class.object_id, UML name, ...).',
+ 'dependencies' => ['name']
+ ],
+
+ 'value' => [
+ 'type' => 'string',
+ 'usage' => 'text/json',
+ 'description' => 'Meta value (JSON formatted).'
+ ]
+
+ ];
+ }
+
+ public static function calcName($self) {
+ $result = [];
+ $self->read(['code', 'reference']);
+ foreach($self as $id => $meta) {
+ $result[$id] = $meta['code'].'.'.$meta['reference'];
+ }
+ return $result;
+ }
+
+}
diff --git a/packages/core/classes/Permission.class.php b/packages/core/classes/Permission.class.php
index 751f4cabf..f2a0c6a24 100644
--- a/packages/core/classes/Permission.class.php
+++ b/packages/core/classes/Permission.class.php
@@ -23,7 +23,7 @@ public static function getColumns() {
'required' => true
],
- // #deprecated
+ // #deprecated (use `getRole()` for each specific Model)
'domain' => [
'type' => 'string',
'description' => "JSON value of the constraints domain (ex. ['creator', '=', '1'])",
@@ -50,8 +50,8 @@ public static function getColumns() {
'rights' => [
'type' => 'integer',
- 'onupdate' => 'onupdateRights',
- 'description' => "Rights binary mask (1: CREATE, 2: READ, 4: WRITE, 8 DELETE, 16: MANAGE)"
+ 'description' => "Rights binary mask (1: CREATE, 2: READ, 4: WRITE, 8 DELETE, 16: MANAGE)",
+ 'dependencies' => ['rights_txt']
],
// virtual field, used in list views
@@ -64,24 +64,20 @@ public static function getColumns() {
];
}
- public static function onupdateRights($om, $ids, $values, $lang) {
- $om->update(__CLASS__, $ids, ['rights_txt' => null], $lang);
- }
-
public static function calcRightsTxt($om, $ids, $lang) {
$res = [];
$values = $om->read(__CLASS__, $ids, ['rights'], $lang);
- foreach($ids as $oid) {
+ foreach($ids as $id) {
$rights_txt = [];
- $rights = $values[$oid]['rights'];
+ $rights = $values[$id]['rights'];
if($rights & QN_R_CREATE) $rights_txt[] = 'create';
if($rights & QN_R_READ) $rights_txt[] = 'read';
if($rights & QN_R_WRITE) $rights_txt[] = 'write';
if($rights & QN_R_DELETE) $rights_txt[] = 'delete';
if($rights & QN_R_MANAGE) $rights_txt[] = 'manage';
- $res[$oid] = implode(', ', $rights_txt);
+ $res[$id] = implode(', ', $rights_txt);
}
return $res;
}
-}
\ No newline at end of file
+}
diff --git a/packages/core/classes/Task.class.php b/packages/core/classes/Task.class.php
index 2e5f23ee0..adca5a738 100644
--- a/packages/core/classes/Task.class.php
+++ b/packages/core/classes/Task.class.php
@@ -112,6 +112,7 @@ public static function getColumns() {
'params' => [
'type' => 'string',
+ 'usage' => 'text/plain',
'description' => "JSON object holding the parameters to relay to the controller."
],
diff --git a/packages/core/classes/alert/Message.class.php b/packages/core/classes/alert/Message.class.php
index 2821d83b1..1950a2b3b 100644
--- a/packages/core/classes/alert/Message.class.php
+++ b/packages/core/classes/alert/Message.class.php
@@ -56,6 +56,13 @@ public static function getColumns() {
'multilang' => true
],
+ 'log' => [
+ 'type' => 'string',
+ 'usage' => 'text/plain',
+ 'description' => "Arbitrary text containing additional details.",
+ 'help' => "The log is meant to provide extra information about the meaning of the message and contains specific notes and object references relating to the context of the alert.",
+ ],
+
'severity' => [
'type' => 'string',
'selection' => [
diff --git a/packages/core/classes/alert/MessageModel.class.php b/packages/core/classes/alert/MessageModel.class.php
index 2bafd9fab..47d6f9aef 100644
--- a/packages/core/classes/alert/MessageModel.class.php
+++ b/packages/core/classes/alert/MessageModel.class.php
@@ -14,7 +14,7 @@ public static function getColumns() {
return [
'name' => [
'type' => 'string',
- 'description' => 'Name of the model, for sytem identification.',
+ 'description' => 'Name of the model, for system identification.',
'required' => true
],
diff --git a/packages/core/classes/setting/SettingValue.class.php b/packages/core/classes/setting/SettingValue.class.php
index bb2825cb8..5df109f35 100644
--- a/packages/core/classes/setting/SettingValue.class.php
+++ b/packages/core/classes/setting/SettingValue.class.php
@@ -42,6 +42,7 @@ public static function getColumns() {
'type' => 'many2one',
'foreign_object' => 'core\User',
'description' => 'User the setting is specific to (optional).',
+ 'default' => 0,
'ondelete' => 'cascade'
],
diff --git a/packages/core/data/appinfo.php b/packages/core/data/appinfo.php
index 833244a88..0e572039c 100644
--- a/packages/core/data/appinfo.php
+++ b/packages/core/data/appinfo.php
@@ -6,7 +6,7 @@
*/
list($params, $providers) = eQual::announce([
- 'description' => 'Retrieve the descriptor of a given App, identified by package and app ID.',
+ 'description' => 'Retrieve the descriptor of a given App (from manifest), identified by package and app ID.',
'params' => [
'package' => [
'type' => 'string',
@@ -42,20 +42,22 @@
if(isset($manifest['apps'])) {
// handle apps using app descriptors
foreach($manifest['apps'] as $descriptor) {
- if(!is_array($descriptor)) {
- // descriptor is a string (app name)
- if($app != $descriptor) {
- continue;
- }
- // lookup in public/{app}/manifest
- if(file_exists("public/$app/manifest.json")) {
- $result = json_decode(file_get_contents("public/$app/manifest.json"), true);
+ if(is_array($descriptor)) {
+ if(isset($descriptor['id']) && $descriptor['id'] == $app) {
+ $result = $descriptor;
+ break;
}
}
- elseif(isset($descriptor['id']) && $descriptor['id'] == $app) {
- $result = $descriptor;
+ else {
+ // descriptor is a string (app name)
+ if($app == $descriptor) {
+ // lookup in public/{app}/manifest
+ if(file_exists("public/$app/manifest.json")) {
+ $result = json_decode(file_get_contents("public/$app/manifest.json"), true);
+ break;
+ }
+ }
}
- break;
}
}
}
diff --git a/packages/core/data/config/apis.php b/packages/core/data/config/apis.php
index a78d99ac9..a1f931783 100644
--- a/packages/core/data/config/apis.php
+++ b/packages/core/data/config/apis.php
@@ -6,11 +6,12 @@
*/
list($params, $providers) = announce([
'description' => "Returns a list of existing API (string identifiers).\nResult is based on json files stored into config/routing.\nExpected format is api_{identifier}",
+ 'deprecated' => true,
'response' => [
'content-type' => 'application/json',
'charset' => 'utf-8'
],
- 'providers' => ['context']
+ 'providers' => ['context']
]);
diff --git a/packages/core/data/config/check-status.php b/packages/core/data/config/check-status.php
deleted file mode 100644
index 46824bf9d..000000000
--- a/packages/core/data/config/check-status.php
+++ /dev/null
@@ -1,60 +0,0 @@
-
- Some Rights Reserved, Cedric Francoys, 2010-2021
- Licensed under GNU LGPL 3 license
-*/
-list($params, $providers) = eQual::announce([
- 'description' => 'Verify major checkpoints to ensure current installation is healthy.',
- 'params' => [
- ],
- 'constants' => ['ENV_MODE', 'AUTH_SECRET_KEY', 'AUTH_ACCESS_TOKEN_VALIDITY', 'AUTH_TOKEN_HTTPS', 'DEFAULT_RIGHTS', 'DEFAULT_LANG', 'DEBUG_LEVEL'],
- 'response' => [
- 'content-type' => 'application/json',
- 'charset' => 'utf-8',
- 'accept-origin' => '*'
- ],
- 'providers' => ['context']
-]);
-
-/**
- * @var \equal\php\Context $context
- * @var \equal\error\Reporter $reporter
- */
-list($context) = [$providers['context']];
-
-$result = [];
-
-if(!defined('ENV_MODE')) {
- $result[] = 'WARN - CFG - [ENV_MODE] It is recommended to explicitly define the mode under which the installation operates.';
-}
-
-if(constant('ENV_MODE') == 'production') {
- if(file_exists(QN_BASEDIR.'/public/console.php')) {
- $result[] = 'WARN - SEC - [ENV_MODE] Allowing logs access in production is a potential security breach.';
- }
- if(constant('AUTH_SECRET_KEY') == 'my_secret_key') {
- $result[] = 'WARN - SEC - [AUTH_SECRET_KEY] Using default secret key in production is a potential security breach.';
- }
- if(constant('AUTH_ACCESS_TOKEN_VALIDITY') > 86400) {
- $result[] = 'WARN - SEC - [AUTH_ACCESS_TOKEN_VALIDITY] Using hight lifespan for access token in production is a potential security breach.';
- }
- if(constant('DEFAULT_RIGHTS') & R_WRITE == R_WRITE) {
- $result[] = 'WARN - SEC - [DEFAULT_RIGHTS] WRITE permission to all users in production is a potential security breach.';
- }
- if(constant('AUTH_TOKEN_HTTPS') == false) {
- $result[] = 'WARN - SEC - [AUTH_TOKEN_HTTPS] It is recommended to exchange auth token over HTTPS only.';
- }
- if(constant('DEBUG_LEVEL') >= QN_REPORT_INFO) {
- $result[] = 'WARN - CFG - [DEBUG_LEVEL] Using a high debug level will generate a large amount of logs and can result in lower performances.';
- }
-}
-
-if(constant('DEFAULT_LANG') != 'en') {
- $result[] = 'WARN - CFG - [DEFAULT_LANG] It is recommended to use \'en\' as default language.';
-}
-
-$context->httpResponse()
- ->status(200)
- ->body($result)
- ->send();
diff --git a/packages/core/data/config/classes.php b/packages/core/data/config/classes.php
index 5de3b8af2..73db75d1a 100644
--- a/packages/core/data/config/classes.php
+++ b/packages/core/data/config/classes.php
@@ -26,7 +26,9 @@
'providers' => ['context']
]);
-
+/**
+ * @var \equal\php\Context $context
+ */
list($context) = [$providers['context']];
diff --git a/packages/core/data/config/constant.php b/packages/core/data/config/constant.php
index 73287d3b7..5921b1b23 100644
--- a/packages/core/data/config/constant.php
+++ b/packages/core/data/config/constant.php
@@ -5,7 +5,7 @@
Licensed under GNU LGPL 3 license
*/
list($params, $providers) = announce([
- 'description' => 'Request the final value of a specific configuration constant (as it is provided when a controller requests it). If the value is crypted, the decrypted value is shown.',
+ 'description' => 'Request the final value of a specific configuration constant (as it is provided when a controller requests it). If the value is encrypted, the decrypted value is shown.',
'params' => [
'constant' => [
'type' => 'string',
@@ -39,4 +39,4 @@
$context->httpResponse()
->status(200)
->body(['result' => constant($params['constant'])])
- ->send();
\ No newline at end of file
+ ->send();
diff --git a/packages/core/data/config/controllers.php b/packages/core/data/config/controllers.php
index 59ae34e02..5ba93f204 100644
--- a/packages/core/data/config/controllers.php
+++ b/packages/core/data/config/controllers.php
@@ -6,18 +6,18 @@
*/
list($params, $providers) = eQual::announce([
'description' => 'Returns the list of controllers defined in given package.',
- 'response' => [
- 'content-type' => 'application/json',
- 'charset' => 'UTF-8',
- 'accept-origin' => '*'
- ],
'params' => [
'package' => [
'description' => 'Name of the package for which the list is requested',
'type' => 'string',
- 'required' => true
+ 'default' => '*'
]
],
+ 'response' => [
+ 'content-type' => 'application/json',
+ 'charset' => 'UTF-8',
+ 'accept-origin' => '*'
+ ],
'access' => [
'visibility' => 'protected'
],
@@ -30,11 +30,26 @@
list($context, $orm) = [$providers['context'], $providers['orm']];
$result = [
- 'apps' => recurse_dir("packages/{$params['package']}/apps", 'php', $params['package']),
- 'actions' => recurse_dir("packages/{$params['package']}/actions", 'php', $params['package']),
- 'data' => recurse_dir("packages/{$params['package']}/data", 'php', $params['package'])
+ 'apps' => [],
+ 'actions' => [],
+ 'data' => []
];
+$packages = eQual::run('get', 'config_packages');
+
+if($params['package'] != '*') {
+ if(!in_array($params['package'], $packages)) {
+ throw new Exception('unknown_package', EQ_ERROR_INVALID_PARAM);
+ }
+ $packages = (array) $params['package'];
+}
+
+foreach($packages as $package) {
+ $result['apps'] = array_merge($result['apps'], recurse_dir("packages/{$package}/apps", 'php', $package));
+ $result['actions'] = array_merge($result['actions'], recurse_dir("packages/{$package}/actions", 'php', $package));
+ $result['data'] = array_merge($result['data'], recurse_dir("packages/{$package}/data", 'php', $package));
+}
+
$context->httpResponse()
->body($result)
->send();
diff --git a/packages/core/data/config/health-check.php b/packages/core/data/config/health-check.php
new file mode 100644
index 000000000..dccba0dcf
--- /dev/null
+++ b/packages/core/data/config/health-check.php
@@ -0,0 +1,90 @@
+
+ Some Rights Reserved, Cedric Francoys, 2010-2021
+ Licensed under GNU LGPL 3 license
+*/
+list($params, $providers) = eQual::announce([
+ 'description' => 'Verify major checkpoints to ensure current installation is healthy.',
+ 'params' => [
+ ],
+ 'constants' => ['ENV_MODE', 'AUTH_SECRET_KEY', 'AUTH_ACCESS_TOKEN_VALIDITY', 'AUTH_TOKEN_HTTPS', 'DEFAULT_RIGHTS', 'DEFAULT_LANG', 'DEBUG_LEVEL'],
+ 'response' => [
+ 'content-type' => 'application/json',
+ 'charset' => 'utf-8',
+ 'accept-origin' => '*'
+ ],
+ 'providers' => ['context']
+]);
+
+/**
+ * @var \equal\php\Context $context
+ */
+list($context) = [$providers['context']];
+
+$result = [];
+try {
+ if(!file_exists(QN_BASEDIR.'/config/schema.json')) {
+ throw new Exception('WARN - CFG - Missing mandatory file `schema.json`.');
+ }
+
+ if(!defined('ENV_MODE')) {
+ $result[] = 'WARN - CFG - [ENV_MODE] It is recommended to explicitly define the mode under which the installation operates.';
+ }
+
+ if(constant('ENV_MODE') == 'production') {
+ if(file_exists(QN_BASEDIR.'/public/console.php')) {
+ $result[] = 'WARN - SEC - [ENV_MODE] Allowing logs access in production is a potential security breach.';
+ }
+ if(constant('AUTH_SECRET_KEY') == 'my_secret_key') {
+ $result[] = 'WARN - SEC - [AUTH_SECRET_KEY] Using default secret key in production is a potential security breach.';
+ }
+ if(constant('AUTH_ACCESS_TOKEN_VALIDITY') > 86400) {
+ $result[] = 'WARN - SEC - [AUTH_ACCESS_TOKEN_VALIDITY] Using hight lifespan for access token in production is a potential security breach.';
+ }
+ if(constant('DEFAULT_RIGHTS') & EQ_R_WRITE == EQ_R_WRITE) {
+ $result[] = 'WARN - SEC - [DEFAULT_RIGHTS] WRITE permission to all users in production is a potential security breach.';
+ }
+ if(constant('AUTH_TOKEN_HTTPS') == false) {
+ $result[] = 'WARN - SEC - [AUTH_TOKEN_HTTPS] It is recommended to exchange auth token over HTTPS only.';
+ }
+ if(constant('DEBUG_LEVEL') >= QN_REPORT_INFO) {
+ $result[] = 'WARN - CFG - [DEBUG_LEVEL] Using a high debug level will generate a large amount of logs and can result in lower performances.';
+ }
+ }
+
+ if(constant('DEFAULT_LANG') != 'en') {
+ $result[] = 'WARN - CFG - [DEFAULT_LANG] It is recommended to use \'en\' as default language.';
+ }
+
+ if(file_exists(QN_BASEDIR.'/config/config.json')) {
+ $data = file_get_contents(QN_BASEDIR.'/config/schema.json');
+ $schema = json_decode($data, true);
+ $data = file_get_contents(QN_BASEDIR.'/config/config.json');
+ $config = json_decode($data, true);
+
+ foreach($config as $key => $value) {
+ if(!isset($schema[$key])) {
+ $result[] = 'WARN - CFG - Unknown config constant ['.$key.'] in `config.json`';
+ continue;
+ }
+ if(isset($schema[$key]['deprecated'])) {
+ $result[] = 'WARN - CFG - ['.$key.'] is marked as deprecated and should not be used in `config.json`';
+ }
+ }
+
+ }
+
+ if(!file_exists(QN_BASEDIR.'/public/assets/env/config.json')) {
+ throw new Exception('WARN - CFG - UI `config.json` not set: default values will apply.');
+ }
+
+}
+catch(Exception $e) {
+ $result[] = $e->getMessage();
+}
+
+$context->httpResponse()
+ ->status(200)
+ ->body($result)
+ ->send();
diff --git a/packages/core/data/config/i18n-menu.php b/packages/core/data/config/i18n-menu.php
index ab83c143d..5a493877c 100644
--- a/packages/core/data/config/i18n-menu.php
+++ b/packages/core/data/config/i18n-menu.php
@@ -4,7 +4,7 @@
Some Rights Reserved, Cedric Francoys, 2010-2021
Licensed under GNU GPL 3 license
*/
-list($params, $providers) = announce([
+list($params, $providers) = eQual::announce([
'description' => "Retrieves the translation values related to the specified menu.",
'params' => [
'package' => [
@@ -29,22 +29,23 @@
'charset' => 'utf-8',
'accept-origin' => '*'
],
- 'providers' => ['context', 'orm']
+ 'providers' => ['context']
]);
-list($context, $orm) = [ $providers['context'], $providers['orm'] ];
+/**
+ * @var \equal\php\Context $context
+ */
+list($context) = [ $providers['context'] ];
+$result = [];
$file = QN_BASEDIR."/packages/{$params['package']}/i18n/{$params['lang']}/menu.{$params['menu_id']}.json";
-if(!file_exists($file)) {
- throw new Exception("unknown_lang_file", QN_ERROR_UNKNOWN_OBJECT);
-}
-
-if( ($schema = json_decode(@file_get_contents($file), true)) === null) {
- throw new Exception("malformed_json", QN_ERROR_INVALID_CONFIG);
+// #memo - to prevent untimely log entries, this script always return a non-404 error
+if(file_exists($file) && ($schema = json_decode(@file_get_contents($file), true)) !== null) {
+ $result = $schema;
}
$context->httpResponse()
- ->body($schema)
- ->send();
\ No newline at end of file
+ ->body($result)
+ ->send();
diff --git a/packages/core/data/config/i18n.php b/packages/core/data/config/i18n.php
index 17591a973..982cef416 100644
--- a/packages/core/data/config/i18n.php
+++ b/packages/core/data/config/i18n.php
@@ -4,9 +4,10 @@
Some Rights Reserved, Cedric Francoys, 2010-2021
Licensed under GNU GPL 3 license
*/
-// #deprecated - use `core_translation` instead
+// #deprecated - use `core_config_translation` instead
list($params, $providers) = announce([
'description' => "Retrieves the translation values related to the specified entity.",
+ 'deprecated' => true,
'params' => [
'entity' => [
'description' => 'Full name (including namespace) of the class to look for (e.g. \'core\\User\').',
diff --git a/packages/core/data/config/init-data.php b/packages/core/data/config/init-data.php
new file mode 100644
index 000000000..f38e792c6
--- /dev/null
+++ b/packages/core/data/config/init-data.php
@@ -0,0 +1,70 @@
+ 'This is the core_config_init-data controller created with core_config_create-controller.',
+ 'response' => [
+ 'charset' => 'utf-8',
+ 'content-type' => 'application/json',
+ 'accept-origin' => ['*'],
+ ],
+ 'params' => [
+ 'package' => [
+ 'type' => 'string',
+ 'usage' => 'orm/package',
+ 'description' => 'Package for which initial data files are requested.',
+ 'default' => 'core',
+ ],
+ 'type' => [
+ 'type' => 'string',
+ 'description' => 'Type of requested init-data (folder).',
+ 'selection' => [
+ 'init',
+ 'demo'
+ ],
+ 'default' => 'init'
+ ],
+ ],
+ 'access' => [
+ 'visibility' => 'protected',
+ 'groups' => ['users'],
+ ],
+ 'providers' => ['context'],
+]);
+
+/** @var \equal\php\context Context */
+$context = $providers['context'];
+
+$package = $params['package'];
+
+$packages = equal::run("get", "config_packages");
+
+if(!in_array($package, $packages)) {
+ throw new Exception("unknown_package", QN_ERROR_INVALID_PARAM);
+}
+
+$folder = ([
+ "init" => "data",
+ "demo" => "demo"
+ ])[$params['type']];
+
+
+$dir = QN_BASEDIR."/packages/$package/init/$folder";
+
+if(!is_dir($dir)) {
+ throw new Exception("missing_directory", QN_ERROR_INVALID_CONFIG);
+}
+
+$result = [];
+
+$files = FSManipulator::getDirFlatten($dir, ['json']);
+
+foreach($files as $file) {
+ $result[$file] = json_decode(file_get_contents("$dir/$file"), true);
+}
+
+$context->httpResponse()
+ ->body($result)
+ ->status(200)
+ ->send();
diff --git a/packages/core/data/config/live/packages.php b/packages/core/data/config/live/packages.php
index 3cbb4a135..e14845037 100644
--- a/packages/core/data/config/live/packages.php
+++ b/packages/core/data/config/live/packages.php
@@ -13,7 +13,12 @@
'response' => [
'content-type' => 'application/json',
'charset' => 'utf-8',
- 'accept-origin' => '*'
+ 'accept-origin' => '*',
+ 'schema' => [
+ 'type' => 'array',
+ 'usage' => 'text[]/plain:255',
+ 'description' => 'List of packages names.'
+ ]
],
'providers' => ['context']
]);
diff --git a/packages/core/data/config/menus.php b/packages/core/data/config/menus.php
new file mode 100644
index 000000000..1f2675e8a
--- /dev/null
+++ b/packages/core/data/config/menus.php
@@ -0,0 +1,98 @@
+
+ Some Rights Reserved, Cedric Francoys, 2010-2021
+ Licensed under GNU LGPL 3 license
+*/
+list($params, $providers) = eQual::announce([
+ 'description' => 'Returns the list of menus defined in a given package, or applicable to a given entity.',
+ 'response' => [
+ 'content-type' => 'application/json',
+ 'charset' => 'UTF-8',
+ 'accept-origin' => '*'
+ ],
+ 'params' => [
+ 'package' => [
+ 'description' => 'Name of the package for which the list is requested.',
+ 'type' => 'string',
+ 'required' => true
+ ],
+ ],
+ 'providers' => ['context', 'orm']
+]);
+
+/**
+ * @var \equal\php\Context $context
+ * @var \equal\orm\ObjectManager $orm
+ */
+list($context, $orm) = [$providers['context'], $providers['orm']];
+
+$result = [];
+
+if(!file_exists("packages/{$params['package']}")) {
+ throw new Exception('missing_package_dir', QN_ERROR_INVALID_CONFIG);
+}
+if(!file_exists("packages/{$params['package']}/views")) {
+ throw new Exception('missing_views_dir', QN_ERROR_INVALID_CONFIG);
+}
+// recurse through all sub-folders of `views` directory
+$result = recurse_dir("packages/{$params['package']}/views", 'json', $params['package']);
+
+
+$context->httpResponse()
+ ->body($result)
+ ->send();
+
+function has_sub_items($directory, $extension) {
+ $files = glob($directory.'/*.'.$extension);
+ if(count($files)) {
+ return true;
+ }
+ foreach(glob($directory.'/*', GLOB_ONLYDIR|GLOB_NOSORT) as $node) {
+ if(has_sub_items($node, $extension)) {
+ return true;
+ }
+ }
+ return false;
+}
+
+/**
+ * #memo - this method slightly differs from the one in controllers.php and translations.php
+ */
+function recurse_dir($directory, $extension, $parent_name='') {
+ $result = array();
+ if( is_dir($directory) ) {
+ $dir_name = basename($directory);
+ $list = glob($directory.'/*');
+ foreach($list as $node) {
+ $filename = basename($node, '.'.$extension);
+ list($entity_name, $view_id) = explode('.', $filename, 2);
+ if(!($entity_name == 'menu')) continue;
+ if(is_dir($node)) {
+ if(!has_sub_items($node, $extension)) {
+ continue;
+ }
+ $result = array_merge($result, recurse_dir($node, $extension, (strlen($parent_name)?$parent_name.'\\'.$filename:$filename)));
+ }
+ elseif(pathinfo($node, PATHINFO_EXTENSION) == $extension) {
+ $entity = (strlen($parent_name)?$parent_name.'\\':'').$entity_name;
+ try {
+ // #memo - ! can be controller or class
+ // $entity::getType();
+ $result[] = $view_id;
+ }
+ catch(Exception $e) {
+ if($entity_name == 'menu') {
+ // ignore
+ }
+ else {
+ // entity should be a controller
+ // #todo - check that the controller actually exists
+ $result[] = str_replace('\\', '_', $entity).':'.$view_id;
+ }
+ }
+ }
+ }
+ }
+ return $result;
+}
diff --git a/packages/core/data/config/translations.php b/packages/core/data/config/translations.php
old mode 100755
new mode 100644
diff --git a/packages/core/data/config/types.php b/packages/core/data/config/types.php
old mode 100755
new mode 100644
index 93535393e..9bf17ee39
--- a/packages/core/data/config/types.php
+++ b/packages/core/data/config/types.php
@@ -33,6 +33,7 @@
$properties_descriptor = [
'usage' => ['type' => 'string', 'description' => 'Specifies additional information about the format of the field.'],
'onupdate' => ['type' => 'string', 'description' => 'Name of the method to invoke when field is updated.'],
+ 'onrevert' => ['type' => 'string', 'description' => 'Name of the method to invoke when a computed field is explicitly set to NULL.'],
'dependencies' => ['type' => 'array', 'description' => 'List of computed fields that must be reset when the value of the field is updated.'],
'selection' => ['type' => 'array', 'description' => 'Pre-defined list or associative array holding the possible values for the field.'],
'unique' => ['type' => 'boolean', 'description' => 'If the property need to be unique.'],
@@ -136,7 +137,10 @@
'computed' => [
'type' => ['type' => 'string'],
//'default' => ['type' => 'string'],
- 'function', 'result_type', 'onupdate', 'store', 'instant', 'multilang', 'selection'
+ 'function', 'result_type', 'onupdate', 'store', 'instant', 'multilang', 'onrevert'
+ ],
+ 'array' => [
+ 'default','type' => ['type' => 'string'],'dependencies','onupdate', 'selection','usage'
]
];
diff --git a/packages/core/data/config/uml.php b/packages/core/data/config/uml.php
new file mode 100644
index 000000000..f4c1c88f9
--- /dev/null
+++ b/packages/core/data/config/uml.php
@@ -0,0 +1,83 @@
+ "Attempts to create a new package using a given name.",
+ 'params' => [
+ 'package' => [
+ 'description' => 'Name of the package of the new model',
+ 'type' => 'string',
+ 'required' => true
+ ],
+ 'path' => [
+ 'decription' => 'relative path to the file from packages/{pkg}/',
+ 'type' => 'string',
+ 'required' => true
+ ],
+ 'type' => [
+ 'description' => 'Type of the UML data',
+ 'type' => 'string',
+ 'required' => true,
+ 'selection' => [
+ 'or'
+ ]
+ ]
+ ],
+ 'response' => [
+ 'content-type' => 'text',
+ 'charset' => 'utf-8',
+ 'accept-origin' => '*'
+ ],
+ 'access' => [
+ 'visibility' => 'protected',
+ 'groups' => ['admins']
+ ],
+ 'providers' => ['context', 'orm', 'access']
+]);
+
+/**
+ * @var \equal\php\Context $context
+ * @var \equal\orm\ObjectManager $orm
+ * @var \equal\access\AccessController $ac
+ */
+list($context, $orm, $ac) = [$providers['context'], $providers['orm'], $providers['access']];
+
+$response_code = 200;
+
+$package = $params["package"];
+
+// Checking if package exists
+if(!file_exists(QN_BASEDIR."/packages/{$package}")) {
+ throw new Exception('missing_package_dir', QN_ERROR_INVALID_PARAM);
+}
+
+// Checking if package exists
+if(!file_exists(QN_BASEDIR."/packages/{$package}/uml")) {
+ throw new Exception('malformed_package', QN_ERROR_INVALID_CONFIG);
+}
+
+$path_arr = explode('/',$params['path']);
+$filename = array_pop($path_arr);
+$path = implode("/",$path_arr);
+
+$path = str_replace("..","",$path);
+
+if(!endsWith($filename,".{$params["type"]}.equml")) {
+ $filename = $filename.".{$params["type"]}.equml";
+}
+
+if(!file_exists(QN_BASEDIR."/packages/{$package}/uml/{$path}/{$filename}")) {
+ throw new Exception('io_error', QN_ERROR_INVALID_CONFIG);
+}
+
+$context->httpResponse()
+ ->body(file_get_contents(QN_BASEDIR."/packages/{$package}/uml/{$path}/{$filename}"))
+ ->status(200)
+ ->send();
+
+function endsWith( $haystack, $needle ) {
+ $length = strlen( $needle );
+ if( !$length ) {
+ return true;
+ }
+ return substr( $haystack, -$length ) === $needle;
+}
\ No newline at end of file
diff --git a/packages/core/data/config/umls.php b/packages/core/data/config/umls.php
new file mode 100644
index 000000000..632dcf766
--- /dev/null
+++ b/packages/core/data/config/umls.php
@@ -0,0 +1,85 @@
+ 'Returns the list of menus defined in a given package, or applicable to a given entity.',
+ 'response' => [
+ 'content-type' => 'application/json',
+ 'charset' => 'UTF-8',
+ 'accept-origin' => '*'
+ ],
+ 'params' => [
+ 'type' => [
+ 'description' => 'Type of the UML data',
+ 'type' => 'string',
+ 'selection' => [
+ 'or'
+ ]
+ ]
+ ],
+ 'providers' => ['context', 'orm']
+]);
+
+/**
+ * @var \equal\php\Context $context
+ * @var \equal\orm\ObjectManager $orm
+ */
+list($context, $orm) = [$providers['context'], $providers['orm']];
+
+$packages = eQual::run('get','core_config_packages',[]);
+
+$result = [];
+
+foreach($packages as $package) {
+ $result[$package] = recurse_dir(QN_BASEDIR."/packages/{$package}/uml","equml",$params['type']);
+}
+
+$context->httpResponse()
+ ->body(json_encode($result))
+ ->send();
+
+function has_sub_items($directory, $extension) {
+ $files = glob($directory.'/*.'.$extension);
+ if(count($files)) {
+ return true;
+ }
+ foreach(glob($directory.'/*', GLOB_ONLYDIR|GLOB_NOSORT) as $node) {
+ if(has_sub_items($node, $extension)) {
+ return true;
+ }
+ }
+ return false;
+}
+
+function endsWith( $haystack, $needle ) {
+ $length = strlen( $needle );
+ if( !$length ) {
+ return true;
+ }
+ return substr( $haystack, -$length ) === $needle;
+}
+
+/**
+ * #memo - this method highily differs from the one in controllers.php , translations.php and menu.php
+*/
+function recurse_dir($directory, $extension,$type,$parent_name='') {
+ $result = array();
+ if( is_dir($directory) ) {
+ $list = glob($directory.'/*');
+ foreach($list as $node) {
+ $filename = basename($node, '.'.$extension);
+ if(is_dir($node)) {
+ if(!has_sub_items($node, $extension)) {
+ continue;
+ }
+ $result = array_merge($result, recurse_dir($node, $extension, $type , $parent_name.'/'.$filename));
+ }
+ elseif(pathinfo($node, PATHINFO_EXTENSION) == $extension && endsWith($filename,$type)) {
+ $entity = (strlen($parent_name)?$parent_name.'/':'').$filename.'.'.$extension;
+ $result[] = $entity;
+ }
+ }
+ }
+ return $result;
+}
+
diff --git a/packages/core/data/config/views.php b/packages/core/data/config/views.php
index 5823a3585..02272597c 100644
--- a/packages/core/data/config/views.php
+++ b/packages/core/data/config/views.php
@@ -132,6 +132,7 @@ function recurse_dir($directory, $extension, $parent_name='') {
foreach($list as $node) {
$filename = basename($node, '.'.$extension);
list($entity_name, $view_id) = explode('.', $filename, 2);
+ if($entity_name == 'menu') continue;
if(is_dir($node)) {
if(!has_sub_items($node, $extension)) {
continue;
diff --git a/packages/core/data/config/widget-types.php b/packages/core/data/config/widget-types.php
old mode 100755
new mode 100644
diff --git a/packages/core/data/model/actions.php b/packages/core/data/model/actions.php
new file mode 100644
index 000000000..040c7d0c6
--- /dev/null
+++ b/packages/core/data/model/actions.php
@@ -0,0 +1,51 @@
+
+ Some Rights Reserved, Cedric Francoys, 2010-2021
+ Licensed under GNU LGPL 3 license
+*/
+
+list($params, $providers) = eQual::announce([
+ 'description' => 'Returns the actions defined for the given entity, if any.',
+ 'help' => 'This controller provides a JSON map of the actions as defined in the class of the given entity. If no `getWorkflow` method is found in the class, it falls back to the one defined in the orm\Model class, which returns an empty array.',
+ 'params' => [
+ 'entity' => [
+ 'description' => 'Full name (including namespace) of the class to look into (e.g. \'core\\User\').',
+ 'type' => 'string',
+ 'usage' => 'orm/entity',
+ 'required' => true
+ ]
+ ],
+ 'response' => [
+ 'content-type' => 'application/json',
+ 'charset' => 'utf-8',
+ 'accept-origin' => '*'
+ ],
+ 'providers' => [ 'context', 'orm' ]
+]);
+
+/**
+ * @var \equal\php\Context $context
+ * @var \equal\orm\ObjectManager $orm
+ */
+list($context, $orm) = [ $providers['context'], $providers['orm'] ];
+
+// retrieve target entity
+$entity = $orm->getModel($params['entity']);
+if(!$entity) {
+ throw new Exception("unknown_entity", QN_ERROR_INVALID_PARAM);
+}
+
+if(!method_exists($entity, 'getActions')) {
+ throw new Exception("missing_method", QN_ERROR_INVALID_CONFIG);
+}
+
+$actions = $entity->getActions();
+
+if(!is_array($actions)) {
+ throw new Exception("invalid_method", QN_ERROR_INVALID_CONFIG);
+}
+
+$context->httpResponse()
+ ->body($actions)
+ ->send();
diff --git a/packages/core/data/model/collect.php b/packages/core/data/model/collect.php
index 2c044ce0a..a0073ac92 100644
--- a/packages/core/data/model/collect.php
+++ b/packages/core/data/model/collect.php
@@ -7,12 +7,17 @@
use equal\orm\Domain;
use equal\orm\Field;
-list($params, $providers) = announce([
+list($params, $providers) = eQual::announce([
'description' => 'Returns a list of entities according to given domain (filter), start offset, limit and order.',
'params' => [
'entity' => [
- 'description' => 'Full name (including namespace) of the class to look into (e.g. \'core\\User\').',
+ 'description' => 'Full name of the entity to collect.',
+ 'help' => "The entity is either a class or a controller, given with its full namespace (including the package).
+ Classes are given with backslash separators (e.g. 'core\\User'),
+ while controllers are given with underscore separators (e.g. 'core_model_collect').
+ If a controller is given, it is expected to be a data handler (GET).",
'type' => 'string',
+ 'usage' => 'orm/entity',
'required' => true
],
'fields' => [
diff --git a/packages/core/data/model/menu.php b/packages/core/data/model/menu.php
index 713534da4..03c7875aa 100644
--- a/packages/core/data/model/menu.php
+++ b/packages/core/data/model/menu.php
@@ -4,7 +4,7 @@
Some Rights Reserved, Cedric Francoys, 2010-2021
Licensed under GNU LGPL 3 license
*/
-list($params, $providers) = announce([
+list($params, $providers) = eQual::announce([
'description' => "Returns the JSON menu related to a package, given a menu ID ().",
'params' => [
'package' => [
@@ -23,23 +23,24 @@
'charset' => 'utf-8',
'accept-origin' => '*'
],
- 'providers' => ['context', 'orm']
+ 'providers' => ['context']
]);
+/**
+ * @var \equal\php\Context $context
+ */
+list($context, $orm) = [ $providers['context'] ];
-list($context, $orm) = [$providers['context'], $providers['orm']];
+$result = [];
// retrieve existing view meant for package
$file = QN_BASEDIR."/packages/{$params['package']}/views/menu.{$params['menu_id']}.json";
-if(!file_exists($file)) {
- throw new Exception("unknown_view_id", QN_ERROR_UNKNOWN_OBJECT);
-}
-
-if( ($view = json_decode(@file_get_contents($file), true)) === null) {
- throw new Exception("malformed_view_schema", QN_ERROR_INVALID_CONFIG);
+// #memo - to prevent untimely log entries, this script always return a non-404 error
+if(file_exists($file) && ($view = json_decode(@file_get_contents($file), true)) !== null) {
+ $result = $view;
}
$context->httpResponse()
->body($view)
- ->send();
\ No newline at end of file
+ ->send();
diff --git a/packages/core/data/model/policies.php b/packages/core/data/model/policies.php
new file mode 100644
index 000000000..82df9d1d9
--- /dev/null
+++ b/packages/core/data/model/policies.php
@@ -0,0 +1,51 @@
+
+ Some Rights Reserved, Cedric Francoys, 2010-2021
+ Licensed under GNU LGPL 3 license
+*/
+
+list($params, $providers) = eQual::announce([
+ 'description' => 'Returns the policies defined for the given entity, if any.',
+ 'help' => 'This controller provides a JSON map of the policies as defined in the class of the given entity. If no `getWorkflow` method is found in the class, it falls back to the one defined in the orm\Model class, which returns an empty array.',
+ 'params' => [
+ 'entity' => [
+ 'description' => 'Full name (including namespace) of the class to look into (e.g. \'core\\User\').',
+ 'type' => 'string',
+ 'usage' => 'orm/entity',
+ 'required' => true
+ ]
+ ],
+ 'response' => [
+ 'content-type' => 'application/json',
+ 'charset' => 'utf-8',
+ 'accept-origin' => '*'
+ ],
+ 'providers' => [ 'context', 'orm' ]
+]);
+
+/**
+ * @var \equal\php\Context $context
+ * @var \equal\orm\ObjectManager $orm
+ */
+list($context, $orm) = [ $providers['context'], $providers['orm'] ];
+
+// retrieve target entity
+$entity = $orm->getModel($params['entity']);
+if(!$entity) {
+ throw new Exception("unknown_entity", QN_ERROR_INVALID_PARAM);
+}
+
+if(!method_exists($entity, 'getPolicies')) {
+ throw new Exception("missing_method", QN_ERROR_INVALID_CONFIG);
+}
+
+$policies = $entity->getPolicies();
+
+if(!is_array($policies)) {
+ throw new Exception("invalid_method", QN_ERROR_INVALID_CONFIG);
+}
+
+$context->httpResponse()
+ ->body($policies)
+ ->send();
diff --git a/packages/core/data/model/roles.php b/packages/core/data/model/roles.php
new file mode 100644
index 000000000..35ab35e45
--- /dev/null
+++ b/packages/core/data/model/roles.php
@@ -0,0 +1,51 @@
+
+ Some Rights Reserved, Cedric Francoys, 2010-2021
+ Licensed under GNU LGPL 3 license
+*/
+
+list($params, $providers) = eQual::announce([
+ 'description' => 'Returns the roles defined for the given entity, if any.',
+ 'help' => 'This controller provides a JSON map of the roles as defined in the class of the given entity. If no `getWorkflow` method is found in the class, it falls back to the one defined in the orm\Model class, which returns an empty array.',
+ 'params' => [
+ 'entity' => [
+ 'description' => 'Full name (including namespace) of the class to look into (e.g. \'core\\User\').',
+ 'type' => 'string',
+ 'usage' => 'orm/entity',
+ 'required' => true
+ ]
+ ],
+ 'response' => [
+ 'content-type' => 'application/json',
+ 'charset' => 'utf-8',
+ 'accept-origin' => '*'
+ ],
+ 'providers' => [ 'context', 'orm' ]
+]);
+
+/**
+ * @var \equal\php\Context $context
+ * @var \equal\orm\ObjectManager $orm
+ */
+list($context, $orm) = [ $providers['context'], $providers['orm'] ];
+
+// retrieve target entity
+$entity = $orm->getModel($params['entity']);
+if(!$entity) {
+ throw new Exception("unknown_entity", QN_ERROR_INVALID_PARAM);
+}
+
+if(!method_exists($entity, 'getRoles')) {
+ throw new Exception("missing_method", QN_ERROR_INVALID_CONFIG);
+}
+
+$roles = $entity->getRoles();
+
+if(!is_array($roles)) {
+ throw new Exception("invalid_method", QN_ERROR_INVALID_CONFIG);
+}
+
+$context->httpResponse()
+ ->body($roles)
+ ->send();
diff --git a/packages/core/data/model/view.php b/packages/core/data/model/view.php
index ba698ca7c..ece41605b 100644
--- a/packages/core/data/model/view.php
+++ b/packages/core/data/model/view.php
@@ -31,18 +31,27 @@
$entity = $params['entity'];
+list($view_type, $view_name) = explode('.', $params['view_id']);
+
// retrieve existing view meant for entity (recurse through parents)
while(true) {
$parts = explode('\\', $entity);
$package = array_shift($parts);
$file = array_pop($parts);
$class_path = implode('/', $parts);
- $file = QN_BASEDIR."/packages/{$package}/views/{$class_path}/{$file}.{$params['view_id']}.json";
+ $file = QN_BASEDIR."/packages/{$package}/views/{$class_path}/{$file}.{$view_type}.{$view_name}.json";
+ if(file_exists($file)) {
+ break;
+ }
+
+ // fallback to default variant of the view
+ $file = QN_BASEDIR."/packages/{$package}/views/{$class_path}/{$file}.{$view_type}.default.json";
if(file_exists($file)) {
break;
}
+ // go one level up through parents
try {
$parent = get_parent_class($orm->getModel($entity));
if(!$parent || $parent == 'equal\orm\Model') {
@@ -57,7 +66,7 @@
}
if(!file_exists($file)) {
- throw new Exception("unknown_view_id", QN_ERROR_UNKNOWN_OBJECT);
+ throw new Exception("missing_view", QN_ERROR_UNKNOWN_OBJECT);
}
if( ($view = json_decode(@file_get_contents($file), true)) === null) {
diff --git a/packages/core/data/model/workflow.php b/packages/core/data/model/workflow.php
new file mode 100644
index 000000000..781480770
--- /dev/null
+++ b/packages/core/data/model/workflow.php
@@ -0,0 +1,57 @@
+
+ Some Rights Reserved, Cedric Francoys, 2010-2021
+ Licensed under GNU LGPL 3 license
+*/
+
+list($params, $providers) = eQual::announce([
+ 'description' => 'Returns the schema of the workflow defined for the given entity, if any.',
+ 'help' => 'This controller provides a JSON map of the workflow as defined in the class of the given entity. If no `getWorkflow` method is found in the class, it falls back to the one defined in the orm\Model class, which returns an empty array.',
+ 'params' => [
+ 'entity' => [
+ 'description' => 'Full name (including namespace) of the class to look into (e.g. \'core\\User\').',
+ 'type' => 'string',
+ 'usage' => 'orm/entity',
+ 'required' => true
+ ]
+ ],
+ 'response' => [
+ 'content-type' => 'application/json',
+ 'charset' => 'utf-8',
+ 'accept-origin' => '*'
+ ],
+ 'providers' => [ 'context', 'orm' ]
+]);
+
+/**
+ * @var \equal\php\Context $context
+ * @var \equal\orm\ObjectManager $orm
+ */
+list($context, $orm) = [ $providers['context'], $providers['orm'] ];
+
+// retrieve target entity
+$entity = $orm->getModel($params['entity']);
+if(!$entity) {
+ throw new Exception("unknown_entity", QN_ERROR_INVALID_PARAM);
+}
+
+if(!method_exists($entity, 'getWorkflow')) {
+ throw new Exception("missing_method", QN_ERROR_INVALID_CONFIG);
+}
+
+$workflow = [];
+
+$reflectionClass = new ReflectionClass($entity::getType());
+if($reflectionClass->getMethod('getWorkflow')->class != $entity::getType()) {
+ throw new Exception("entity_without_workflow", QN_ERROR_UNKNOWN_OBJECT);
+}
+
+$workflow = $entity->getWorkflow();
+if(!is_array($workflow)) {
+ throw new Exception("invalid_method", QN_ERROR_INVALID_CONFIG);
+}
+
+$context->httpResponse()
+ ->body($workflow)
+ ->send();
diff --git a/packages/core/data/packageinfo.php b/packages/core/data/packageinfo.php
index d695241f5..85b1f56e9 100644
--- a/packages/core/data/packageinfo.php
+++ b/packages/core/data/packageinfo.php
@@ -14,7 +14,7 @@
],
'access' => [
'visibility' => 'protected',
- 'groups' => ['admin']
+ 'groups' => ['admins']
],
'response' => [
'content-type' => 'application/json',
diff --git a/packages/core/data/phpinfo.php b/packages/core/data/phpinfo.php
index bf5c41b61..f5c69e013 100644
--- a/packages/core/data/phpinfo.php
+++ b/packages/core/data/phpinfo.php
@@ -4,8 +4,16 @@
Some Rights Reserved, Cedric Francoys, 2010-2021
Licensed under GNU LGPL 3 license
*/
-list($params, $providers) = eQual::announce([
+$params = eQual::announce([
'description' => 'Outputs plain text version of PHP current configuration (from `phpinfo`).',
+ 'help' => 'This controller might reveal details about current PHP config. It is therefore marked as private and is meant to be used in CLI only.',
+ 'params' => [
+ 'json' => [
+ 'type' => 'boolean',
+ 'default' => false,
+ 'description' => 'Force output to a JSON formatted string.'
+ ]
+ ],
'access' => [
'visibility' => 'private'
],
@@ -16,4 +24,87 @@
]
]);
-echo phpinfo();
+$info = phpinfo_array();
+
+if($params['json']) {
+ echo json_encode($info, JSON_PRETTY_PRINT);
+}
+else {
+ $i = 0;
+ foreach($info as $section => $content) {
+ if($i > 0) {
+ echo PHP_EOL;
+ }
+ echo $section.PHP_EOL;
+ echo str_pad('', strlen($section), '-').PHP_EOL;
+ foreach($content as $subsection => $value) {
+ if(is_array($value)) {
+ echo ' '.$subsection.PHP_EOL;
+ echo ' '.str_pad('', strlen($subsection), '-').PHP_EOL;
+ foreach($value as $key => $val) {
+ echo ' '.' '.str_pad($key, 41, ' ').'=> '.$val.PHP_EOL;
+ }
+ }
+ else {
+ echo ' '.str_pad($subsection, 41, ' ').'=> '.$value.PHP_EOL;
+ }
+ }
+ ++$i;
+ }
+}
+
+
+function phpinfo_array(){
+ ob_start();
+ phpinfo(INFO_GENERAL|INFO_CONFIGURATION|INFO_MODULES);
+ $output = ob_get_clean();
+
+ $map_info = [];
+ $data = explode("\n", $output);
+ $section = '';
+ $subsection = null;
+ foreach($data as $line) {
+ if(strlen(trim($line)) <= 0) {
+ continue;
+ }
+ if(strpos($line, "\e[1m") !== false) {
+ continue;
+ }
+ if(strpos($line, "Directive =>") !== false) {
+ continue;
+ }
+ if(strpos($line, '=>') === false) {
+ // if there is a tab, it is a subsection
+ if(strpos($line, " ") !== false) {
+ $subsection = trim($line);
+ }
+ else {
+ if(strpos($line, ',') !== false) {
+ // ignore no section line
+ continue;
+ }
+ $section = trim($line);
+ $subsection = null;
+ }
+ }
+ else {
+ if(!isset($map_info[$section])) {
+ $map_info[$section] = [];
+ }
+ if($subsection && !isset($map_info[$section][$subsection])) {
+ $map_info[$section][$subsection] = [];
+ }
+ list($key, $value) = explode('=>', $line, 2);
+ $key = trim($key);
+ $key = str_replace("\e[0m", '', $key);
+ if($subsection) {
+ $map_info[$section][$subsection][$key] = trim($value);
+ }
+ else {
+ $map_info[$section][$key] = trim($value);
+ }
+ }
+ }
+
+ return $map_info;
+}
\ No newline at end of file
diff --git a/packages/core/data/translation.php b/packages/core/data/translation.php
index 8ddc6e0a8..2ac968f30 100644
--- a/packages/core/data/translation.php
+++ b/packages/core/data/translation.php
@@ -5,7 +5,7 @@
Licensed under GNU GPL 3 license
*/
list($params, $providers) = eQual::announce([
- 'description' => "Retrieves the translation values related to the specified entity.",
+ 'description' => "Recursively retrieves the full map of translation values relating to a specified entity.",
'params' => [
'entity' => [
'description' => 'Full name (including namespace) of the class to look for (e.g. \'core\\User\').',
diff --git a/packages/core/data/user/rights.php b/packages/core/data/user/rights.php
index 597fca72b..3df46b7b7 100644
--- a/packages/core/data/user/rights.php
+++ b/packages/core/data/user/rights.php
@@ -26,28 +26,27 @@
'default' => '*'
]
],
- 'providers' => ['context', 'auth', 'access', 'orm']
+ 'providers' => ['context', 'access']
]);
-list($context, $orm, $am, $ac) = [ $providers['context'], $providers['orm'], $providers['auth'], $providers['access'] ];
+list($context, $access) = [ $providers['context'], $providers['access'] ];
// retrieve targeted user
if(is_numeric($params['user'])) {
$ids = User::search(['id', '=', $params['user']])->ids();
- if(!count($ids)) {
- $rights = constant('DEFAULT_RIGHTS');
- }
}
else {
// retrieve by login
$ids = User::search(['login', '=', $params['user']])->ids();
- if(!count($ids)) {
- throw new \Exception("unknown_username", QN_ERROR_UNKNOWN_OBJECT);
- }
- $user_id = array_shift($ids);
- $rights = $ac->getUserRights($user_id, $params['entity']);
}
+if(!count($ids)) {
+ throw new Exception("unknown_user", QN_ERROR_UNKNOWN_OBJECT);
+}
+
+$user_id = array_shift($ids);
+$rights = $access->getUserRights($user_id, $params['entity']);
+
// convert ACL value to human string
$rights_txt = [];
$operations = [
@@ -57,13 +56,21 @@
QN_R_DELETE => 'delete',
QN_R_MANAGE => 'manage'
];
-foreach($operations as $id => $name) {
- if($rights & $id) {
+
+foreach($operations as $op => $name) {
+ if($rights & $op) {
$rights_txt[] = $name;
}
}
+$result = [
+ 'user_id' => $user_id,
+ 'entity' => $params['entity'],
+ 'rights' => $rights,
+ 'rights_txt' => $rights_txt
+];
+
$context->httpResponse()
->status(200)
- ->body(['result' => implode(', ', $rights_txt)])
- ->send();
\ No newline at end of file
+ ->body($result)
+ ->send();
diff --git a/packages/core/i18n/es/Group.json b/packages/core/i18n/es/Group.json
old mode 100755
new mode 100644
diff --git a/packages/core/i18n/es/Mail.json b/packages/core/i18n/es/Mail.json
old mode 100755
new mode 100644
diff --git a/packages/core/i18n/es/Permission.json b/packages/core/i18n/es/Permission.json
old mode 100755
new mode 100644
diff --git a/packages/core/i18n/es/User.json b/packages/core/i18n/es/User.json
old mode 100755
new mode 100644
diff --git a/packages/core/i18n/es/setting/Setting.json b/packages/core/i18n/es/setting/Setting.json
old mode 100755
new mode 100644
diff --git a/packages/core/i18n/es/setting/SettingChoice.json b/packages/core/i18n/es/setting/SettingChoice.json
old mode 100755
new mode 100644
diff --git a/packages/core/i18n/es/setting/SettingValue.json b/packages/core/i18n/es/setting/SettingValue.json
old mode 100755
new mode 100644
diff --git a/packages/core/i18n/fr/locale.json b/packages/core/i18n/fr/locale.json
index 4ed08f75f..49bfd4e3f 100644
--- a/packages/core/i18n/fr/locale.json
+++ b/packages/core/i18n/fr/locale.json
@@ -1,4 +1,5 @@
{
+ "#memo #todo": "could terms and formats be referenced as usages ? ex.: number/boolean.true , date/plain.short , time/plain.full",
"terms": {
"bool.true": "vrai",
"bool.false": "faux",
@@ -46,13 +47,18 @@
"numbers.decimal_separator": ",",
"currency.symbol_position": "after",
"currency.symbol_separator": " ",
+ "date.short.day": "ddd DD/MM/YY",
"date.short": "DD/MM/YY",
"date.medium": "DD/MMM/YYYY",
"date.long": "ddd DD MMM YYYY",
"date.full": "dddd DD MMMM YYYY",
- "time.short": "H:mm",
+ "time.short": "HH:mm",
"time.medium": "HH:mm",
"time.long": "HH:mm:ss",
- "time.full": "HH:mm:ss.SSS"
+ "time.full": "HH:mm:ss.SSS",
+ "datetime.short": "DD/MM/YY HH:mm",
+ "datetime.medium": "DD/MMM/YYYY HH:mm",
+ "datetime.long": "ddd DD MMM YYYY HH:mm",
+ "datetime.full": "dddd DD MMMM YYYY HH:mm"
}
}
\ No newline at end of file
diff --git a/packages/core/init/data/core_setting_SettingValue.json b/packages/core/init/data/core_setting_SettingValue.json
index 5d9064106..159ec9823 100644
--- a/packages/core/init/data/core_setting_SettingValue.json
+++ b/packages/core/init/data/core_setting_SettingValue.json
@@ -1,116 +1,116 @@
[
- {
+ {
"name": "core\\setting\\SettingValue",
"lang": "en",
"data": [
{
"id": 1,
- "name": "numbers.thousands_separator",
+ "name": "core.locale.numbers.thousands_separator",
"setting_id": 1,
"value": "."
},
{
"id": 2,
- "name": "currency.symbol_position",
+ "name": "core.locale.currency.symbol_position",
"setting_id": 2,
"value": "after"
},
{
"id": 3,
- "name": "currency.decimal_precision",
+ "name": "core.locale.currency.decimal_precision",
"setting_id": 3,
"value": "2"
},
{
"id": 4,
- "name": "numbers.decimal_separator",
+ "name": "core.locale.numbers.decimal_separator",
"setting_id": 4,
"value": "."
},
{
"id": 5,
- "name": "numbers.decimal_precision",
+ "name": "core.locale.numbers.decimal_precision",
"setting_id": 5,
"value": "2"
},
{
"id": 6,
- "name": "date_format",
+ "name": "core.locale.date_format",
"setting_id": 6,
"value": "d\/m\/Y"
},
{
"id": 7,
- "name": "time_format",
+ "name": "core.locale.time_format",
"setting_id": 7,
"value": "H:i"
},
{
"id": 8,
- "name": "company.id",
+ "name": "core.main.company.id",
"setting_id": 8,
"value": "1"
},
{
"id": 9,
- "name": "formats.paper",
+ "name": "core.main.formats.paper",
"setting_id": 9,
"value": "A4"
},
{
"id": 10,
- "name": "account_creation",
+ "name": "core.security.account_creation",
"setting_id": 10,
"value": null
},
{
"id": 11,
- "name": "import",
+ "name": "core.security.import",
"setting_id": 11,
"value": "1"
},
{
"id": 12,
- "name": "export",
+ "name": "core.security.export",
"setting_id": 12,
"value": "1"
},
{
"id": 13,
- "name": "currency",
+ "name": "core.units.currency",
"setting_id": 13,
"value": "€"
},
{
"id": 14,
- "name": "length",
+ "name": "core.units.length",
"setting_id": 14,
"value": "m"
},
{
"id": 15,
- "name": "weight",
+ "name": "core.units.weight",
"setting_id": 15,
"value": "kg"
},
{
"id": 16,
- "name": "volume",
+ "name": "core.units.volume",
"setting_id": 16,
"value": "m3"
},
{
"id": 17,
- "name": "surface",
+ "name": "core.units.surface",
"setting_id": 17,
"value": "m2"
},
{
"id": 18,
- "name": "Europe/Brussels",
+ "name": "core.locale.time_zone",
"setting_id": 18,
"value": "Europe/Brussels"
}
]
}
-]
\ No newline at end of file
+]
diff --git a/packages/core/manifest.json b/packages/core/manifest.json
index 7c86c1f45..f3d2d2050 100644
--- a/packages/core/manifest.json
+++ b/packages/core/manifest.json
@@ -5,6 +5,6 @@
"authors": ["Cedric Francoys"],
"license": "LGPL-3",
"depends_on": [],
- "apps": [ "apps", "auth", "app", "settings" ],
+ "apps": [ "apps", "auth", "app", "settings","workbench" ],
"tags": ["equal", "core"]
}
\ No newline at end of file
diff --git a/packages/core/tests/access.php b/packages/core/tests/access.php
index d4727c40f..433a46ef5 100644
--- a/packages/core/tests/access.php
+++ b/packages/core/tests/access.php
@@ -139,7 +139,7 @@
'description' => "Check root user rights.",
'assert' => function() use($providers) {
$access = $providers['access'];
- return $access->hasRight(QN_ROOT_USER_ID, R_MANAGE, 'core\User');
+ return $access->hasRight(QN_ROOT_USER_ID, EQ_R_MANAGE, 'core\User');
},
],
@@ -147,7 +147,7 @@
'description' => "Re-Check root user rights (to ensure using the rights cache).",
'assert' => function() use($providers) {
$access = $providers['access'];
- return $access->hasRight(QN_ROOT_USER_ID, R_MANAGE, 'core\User');
+ return $access->hasRight(QN_ROOT_USER_ID, EQ_R_MANAGE, 'core\User');
},
],
@@ -160,7 +160,7 @@
},
'assert' => function($user_id) use($providers) {
$access = $providers['access'];
- return !$access->hasRight($user_id, R_MANAGE, 'core\User');
+ return !$access->hasRight($user_id, EQ_R_MANAGE, 'core\User');
},
'rollback' => function() {
User::search(['login', '=', 'user_test_1@example.com'])->delete(true);
@@ -179,13 +179,13 @@
},
'act' => function ($group_id) use($providers) {
$access = $providers['access'];
- $access->grantGroups($group_id, R_READ|R_WRITE|R_MANAGE, '*');
+ $access->grantGroups($group_id, EQ_R_READ|EQ_R_WRITE|EQ_R_MANAGE, '*');
return $group_id;
},
'assert' => function($group_id) use($providers) {
$access = $providers['access'];
$user = User::search(['groups_ids', 'contains', $group_id])->first();
- return $access->hasRight($user['id'], R_READ|R_WRITE|R_MANAGE, '*');
+ return $access->hasRight($user['id'], EQ_R_READ|EQ_R_WRITE|EQ_R_MANAGE, '*');
},
'rollback' => function() {
Group::search(['name', '=', 'test1'])->delete(true);
@@ -201,20 +201,20 @@
$access = $providers['access'];
$user = User::create(['login' => 'user_test_1@example.com', 'password' => 'abcd1234'])->first();
$group = Group::create(['name' => 'test1'])->first();
- $access->grantGroups($group['id'], R_MANAGE, '*');
+ $access->grantGroups($group['id'], EQ_R_MANAGE, '*');
$access->addGroup($group['id'], $user['id']);
return [$user['id'], $group['id']];
},
'act' => function ($data) use($providers) {
list($user_id, $group_id) = $data;
$access = $providers['access'];
- $access->revokeGroups($group_id, R_MANAGE, '*');
+ $access->revokeGroups($group_id, EQ_R_MANAGE, '*');
return $data;
},
'assert' => function($data) use($providers) {
list($user_id, $group_id) = $data;
$access = $providers['access'];
- return !$access->hasRight($user_id, R_MANAGE, 'core\User', 1);
+ return !$access->hasRight($user_id, EQ_R_MANAGE, 'core\User', 1);
},
'rollback' => function() {
Group::search(['name', '=', 'test1'])->delete(true);
@@ -238,4 +238,4 @@
User::search(['login', '=', 'user_test_1@example.com'])->delete(true);
}
]
-];
\ No newline at end of file
+];
diff --git a/packages/core/tests/orm.php b/packages/core/tests/orm.php
index eb550b479..cb11cef09 100644
--- a/packages/core/tests/orm.php
+++ b/packages/core/tests/orm.php
@@ -243,7 +243,7 @@
'2220' => [
'description' => "Create a group (no validation)",
'return' => array('integer'),
- 'test' => function () {
+ 'act' => function () {
$om = ObjectManager::getInstance();
$group_id = $om->create('core\Group', ['name' => 'test']);
return $group_id;
@@ -268,7 +268,7 @@
'assert' => function($result) {
return ($result > 0);
},
- 'test' => function () {
+ 'act' => function () {
$om = ObjectManager::getInstance();
$dummy_user_id = $om->search('core\Group', ['login', '=', 'dummy@example.com']);
return $om->remove('core\User', $dummy_user_id, true);
@@ -345,7 +345,7 @@
'2620' => array(
'description' => "Search for some object : clause 'contains' on many2many field",
'return' => array('integer', 'array'),
- 'test' => function () use($providers) {
+ 'arrange' => function () use($providers) {
try {
$providers['auth']->authenticate('cedric@equal.run', 'safe_pass');
// grant READ operation on all classes
@@ -362,7 +362,7 @@
return $values;
},
'assert' => function($result) {
- return (
+ return is_array($result) && count($result) == 2 && (
count(array_diff(['id' => 1, 'login' => 'root@host.local'], (array) $result['1'])) == 0
&& count(array_diff(['id' => 2, 'login' => 'cedric@equal.run'], (array) $result['2'])) == 0
);
@@ -372,7 +372,7 @@
'2631' => array(
'description' => "Add a user to a given group",
'return' => array('integer', 'array'),
- 'test' => function () use($providers) {
+ 'act' => function () use($providers) {
try {
// grant READ operation on all classes
$providers['access']->addGroup(2);
@@ -414,7 +414,7 @@
'3101' => array(
'description' => "Search for an existing user object using Collection (result as map)",
'return' => array('integer', 'array'),
- 'test' => function () {
+ 'act' => function () {
try {
$values = User::search(['login', 'like', 'cedric@equal.run'])
->read(['login'])
@@ -436,7 +436,7 @@
'3102' => array(
'description' => "Search for an existing user object using Collection (result as array)",
'return' => array('integer', 'array'),
- 'test' => function () {
+ 'act' => function () {
try {
$values = User::search(['login', '=', 'cedric@equal.run'])
->read(['login'])
@@ -476,7 +476,7 @@
$om->remove('core\User', $ids, true);
$providers['access']->revoke(QN_R_CREATE|QN_R_DELETE);
},
- 'test' => function () {
+ 'act' => function () {
try {
$values = User::search(['login', '=', 'test@equal.run'])
->read(['login'])
diff --git a/packages/core/uml/overview.or.equml b/packages/core/uml/overview.or.equml
new file mode 100644
index 000000000..554cd67e4
--- /dev/null
+++ b/packages/core/uml/overview.or.equml
@@ -0,0 +1 @@
+[{"entity":"core\\User","hidden":["deleted","created","creator","modified","modifier"],"position":{"x":181,"y":117},"showInheritance":true,"showRelations":true},{"entity":"core\\Group","hidden":["deleted","created","creator","modified","modifier"],"position":{"x":484,"y":112},"showInheritance":true,"showRelations":true},{"entity":"core\\setting\\Setting","hidden":["deleted","created","creator","modified","modifier"],"position":{"x":741,"y":413},"showInheritance":true,"showRelations":true},{"entity":"core\\setting\\SettingChoice","hidden":["deleted","created","creator","modified","modifier"],"position":{"x":402,"y":626},"showInheritance":true,"showRelations":true},{"entity":"core\\setting\\SettingValue","hidden":["deleted","created","creator","modified","modifier"],"position":{"x":403,"y":442},"showInheritance":true,"showRelations":true},{"entity":"core\\Permission","hidden":["deleted","created","creator","modified","modifier"],"position":{"x":699,"y":112},"showInheritance":true,"showRelations":true},{"entity":"core\\Lang","hidden":["deleted","created","creator","modified","modifier"],"position":{"x":1089,"y":369},"showInheritance":true,"showRelations":true},{"entity":"core\\Assignment","hidden":["deleted","created","creator","modified","modifier"],"position":{"x":-87,"y":32},"showInheritance":true,"showRelations":true},{"entity":"core\\Log","hidden":["deleted","created","creator","modified","modifier"],"position":{"x":-2,"y":-180},"showInheritance":true,"showRelations":true},{"entity":"core\\Mail","hidden":["deleted","created","creator","modified","modifier"],"position":{"x":234,"y":-205},"showInheritance":true,"showRelations":true},{"entity":"core\\Meta","hidden":["deleted","created","creator","modified","modifier"],"position":{"x":18,"y":523},"showInheritance":true,"showRelations":true},{"entity":"core\\Task","hidden":["deleted","created","creator","modified","modifier"],"position":{"x":886,"y":-131},"showInheritance":true,"showRelations":true},{"entity":"core\\TaskLog","hidden":["deleted","created","creator","modified","modifier"],"position":{"x":1093,"y":-66},"showInheritance":true,"showRelations":true},{"entity":"core\\Translation","hidden":["deleted","created","creator","modified","modifier"],"position":{"x":1290,"y":369},"showInheritance":true,"showRelations":true},{"entity":"core\\Version","hidden":["deleted","created","creator","modified","modifier"],"position":{"x":1300,"y":128},"showInheritance":true,"showRelations":true},{"entity":"core\\alert\\Message","hidden":["deleted","created","creator","modified","modifier"],"position":{"x":502,"y":-246},"showInheritance":true,"showRelations":true},{"entity":"core\\alert\\MessageModel","hidden":["deleted","created","creator","modified","modifier"],"position":{"x":784,"y":-366},"showInheritance":true,"showRelations":true},{"entity":"core\\setting\\SettingSection","hidden":["deleted","created","creator","modified","modifier"],"position":{"x":987,"y":604},"showInheritance":true,"showRelations":true}]
\ No newline at end of file
diff --git a/packages/core/views/Permission.form.default.json b/packages/core/views/Permission.form.default.json
index f2e2e2ccf..a9e7307c1 100644
--- a/packages/core/views/Permission.form.default.json
+++ b/packages/core/views/Permission.form.default.json
@@ -53,7 +53,10 @@
{
"type": "field",
"value": "group_id",
- "width": "100%"
+ "width": "100%",
+ "widget": {
+ "limit": 100
+ }
},
{
"type": "field",
diff --git a/packages/core/views/User.list.default.json b/packages/core/views/User.list.default.json
index 3db4d0938..74d74e503 100644
--- a/packages/core/views/User.list.default.json
+++ b/packages/core/views/User.list.default.json
@@ -38,6 +38,24 @@
"width": "30%",
"sortable": true
},
+ {
+ "type": "field",
+ "value": "fullname",
+ "width": "20%",
+ "widget": {
+ "readonly": true
+ }
+ },
+ {
+ "type": "field",
+ "value": "firstname",
+ "width": "20%"
+ },
+ {
+ "type": "field",
+ "value": "lastname",
+ "width": "20%"
+ },
{
"type": "field",
"value": "language",
diff --git a/packages/core/views/alert/Message.form.default.json b/packages/core/views/alert/Message.form.default.json
index 63d539dc4..50bab9d48 100644
--- a/packages/core/views/alert/Message.form.default.json
+++ b/packages/core/views/alert/Message.form.default.json
@@ -1,37 +1,80 @@
{
"name": "Message",
"description": "Automated alert message.",
+ "actions": [
+ {
+ "id": "action.retry",
+ "label": "Re-check",
+ "description": "Retry the check that generated the alert. Message will be dismissed if alert is no longer present.",
+ "controller": "core_alert_dismiss",
+ "confirm": false
+ }
+ ],
"layout": {
"groups": [
{
"sections": [
{
+ "id": "section.details",
+ "label": "Details",
"rows": [
{
"columns": [
{
- "width": "100%",
+ "width": "50%",
"items": [
{
"type": "field",
+ "label": "Object Class",
"value": "object_class",
"width": "50%"
},
{
"type": "field",
+ "label": "Object identifier",
"value": "object_id",
- "width": "50%"
+ "width": "25%"
},
{
"type": "field",
"value": "message_model_id",
"width": "50%"
+ }
+ ]
+ },
+ {
+ "width": "50%",
+ "items": [
+ {
+ "type": "field",
+ "value": "label",
+ "width": "50%"
+ },
+ {
+ "type": "field",
+ "value": "description",
+ "width": "100%"
},
{
"type": "field",
"value": "severity",
"width": "50%"
- },
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "id": "section.advanced",
+ "label": "Advanced",
+ "rows": [
+ {
+ "columns": [
+ {
+ "width": "100%",
+ "items": [
{
"type": "field",
"value": "controller",
@@ -41,6 +84,11 @@
"type": "field",
"value": "params",
"width": "50%"
+ },
+ {
+ "type": "field",
+ "value": "log",
+ "width": "100%"
}
]
}
diff --git a/packages/core/views/alert/Message.list.dashboard.json b/packages/core/views/alert/Message.list.dashboard.json
index 048a14f77..ce84bf664 100644
--- a/packages/core/views/alert/Message.list.dashboard.json
+++ b/packages/core/views/alert/Message.list.dashboard.json
@@ -1,16 +1,32 @@
{
"name": "Messages list",
"description": "Listing of pending messages.",
+ "header": {
+ "actions": {
+ "ACTION.CREATE": false
+ },
+ "selection": {
+ "default" : false,
+ "actions" : [
+ {
+ "id": "header.selection.actions.bulk_dismiss",
+ "label": "Re-check",
+ "icon": "replay",
+ "controller": "core_alert_bulk-dismiss"
+ }
+ ]
+ }
+ },
"layout": {
"items": [
{
"type": "field",
- "value": "object_link",
+ "value": "label",
"width": "25%"
},
{
"type": "field",
- "value": "label",
+ "value": "object_link",
"width": "25%"
},
{
diff --git a/packages/core/views/menu.settings.left.json b/packages/core/views/menu.settings.left.json
index b3d5b98cf..83f6bbdec 100644
--- a/packages/core/views/menu.settings.left.json
+++ b/packages/core/views/menu.settings.left.json
@@ -25,6 +25,7 @@
"id": "permissions",
"type": "entry",
"label": "Permissions",
+ "icon": "how_to_reg",
"description": "",
"context": {
"entity": "core\\Permission",
@@ -35,6 +36,7 @@
"id": "users",
"type": "entry",
"label": "Users",
+ "icon": "person",
"description": "",
"context": {
"entity": "core\\User",
@@ -45,6 +47,7 @@
"id": "groups",
"type": "entry",
"label": "Groups",
+ "icon": "group",
"description": "",
"context": {
"entity": "core\\Group",
@@ -66,7 +69,7 @@
"label": "Identities",
"description": "",
"context": {
- "entity": "lodging\\identity\\Identity",
+ "entity": "identity\\Identity",
"view": "list.default"
}
},
diff --git a/packages/core/views/menu.workbench.left.json b/packages/core/views/menu.workbench.left.json
old mode 100644
new mode 100755
index 328a438b6..9baa63bb7
--- a/packages/core/views/menu.workbench.left.json
+++ b/packages/core/views/menu.workbench.left.json
@@ -1,27 +1,35 @@
{
"name": "Workbench menu",
"access": {
- "groups": ["users"]
+ "groups": [
+ "users"
+ ]
},
"layout": {
"items": [
{
- "id": "models",
"type": "entry",
- "label": "Models",
- "description": ""
+ "label": "Components",
+ "icon": "category",
+ "context": [],
+ "children": []
},
{
- "id": "controllers",
- "type": "entry",
- "label": "Controllers",
- "description": ""
- },
- {
- "id": "routes",
- "type": "entry",
- "label": "Routes",
- "description": ""
+ "id": "uml.drawer",
+ "type": "parent",
+ "label": "UML",
+ "icon": "account_tree",
+ "context": [],
+ "children": [
+ {
+ "id": "uml",
+ "type": "entry",
+ "label": "Object-relational",
+ "icon": "schema",
+ "context": [],
+ "children": []
+ }
+ ]
}
]
}
diff --git a/packages/core/views/setting/Setting.form.default.json b/packages/core/views/setting/Setting.form.default.json
index 98a81dadb..56fdd9216 100644
--- a/packages/core/views/setting/Setting.form.default.json
+++ b/packages/core/views/setting/Setting.form.default.json
@@ -6,6 +6,8 @@
{
"sections": [
{
+ "id": "section.details",
+ "label": "Details",
"rows": [
{
"columns": [
@@ -72,6 +74,26 @@
]
}
]
+ },
+ {
+ "id": "section.values",
+ "label": "Setting values",
+ "rows": [
+ {
+ "columns": [
+ {
+ "width": "100%",
+ "items": [
+ {
+ "type": "field",
+ "value": "setting_values_ids",
+ "width": "100%"
+ }
+ ]
+ }
+ ]
+ }
+ ]
}
]
}
diff --git a/packages/demo/data/cities.php b/packages/demo/data/cities.php
index 23d2b3024..8e1a0f71b 100644
--- a/packages/demo/data/cities.php
+++ b/packages/demo/data/cities.php
@@ -1,16 +1,25 @@
'Get list of cities with pictures using teleport API.',
+ 'params' => [
+ ],
+ 'response' => [
+ 'content-type' => 'application/json',
+ 'charset' => 'utf-8'
+ ],
+ 'providers' => ['context']
+]);
$json = file_get_contents('https://api.teleport.org/api/urban_areas/?embed=ua:item/ua:images');
-
$data = json_decode($json, true);
$cities = [];
foreach($data['_embedded']['ua:item'] as $item) {
- echo "'{$item['slug']}' => '{$item['_embedded']['ua:images']['photos'][0]['image']['mobile']}',\n";
-
$cities[$item['slug']] = $item['_embedded']['ua:images']['photos'][0]['image']['mobile'];
}
-echo json_encode($cities, JSON_PRETTY_PRINT);
\ No newline at end of file
+$providers['context']
+ ->httpResponse()
+ ->body($cities)
+ ->send();
diff --git a/packages/demo/data/image.php b/packages/demo/data/image.php
index 41afa7335..6cb36207d 100644
--- a/packages/demo/data/image.php
+++ b/packages/demo/data/image.php
@@ -5,7 +5,7 @@
* HTTP native support
*
*/
-list($params, $providers) = announce([
+list($params, $providers) = eQual::announce([
'description' => 'Get picture data from imgur.com using imgur API.',
'params' => [
'id' => [
@@ -26,7 +26,7 @@
$response = $request
->header('Authorization', "Client-ID 34030ab1f5ef12d")
->send();
-
+
$providers['context']
->httpResponse()
->body(['result' => $response->body()])
diff --git a/packages/demo/views/User.list.default.json b/packages/demo/views/User.list.default.json
old mode 100755
new mode 100644
diff --git a/packages/demo/views/menu.app.left.json b/packages/demo/views/menu.app.left.json
old mode 100755
new mode 100644
diff --git a/public/.htaccess b/public/.htaccess
index b048da9ae..2332f0d09 100644
--- a/public/.htaccess
+++ b/public/.htaccess
@@ -5,8 +5,8 @@ DirectoryIndex index.php index.html
RewriteEngine On
RewriteBase /
-
- RewriteRule ^(index|console|console_json)\.php(\??.*)$ - [L]
+
+ RewriteRule ^(equal|index|console)\.php(\??.*)$ - [L]
RewriteCond %{REQUEST_FILENAME} ^(.*)\.php$ [OR]
RewriteCond %{REQUEST_FILENAME} !-f
diff --git a/public/assets/env/default.json b/public/assets/env/default.json
index 97551d49f..4b693f89b 100644
--- a/public/assets/env/default.json
+++ b/public/assets/env/default.json
@@ -2,7 +2,7 @@
"production": true,
"parent_domain": "equal.local",
"backend_url": "http://equal.local",
- "rest_api_url": "http://equal.local",
+ "rest_api_url": "http://equal.local/",
"lang": "en",
"locale": "en",
"company_name": "eQual Framework",
diff --git a/public/assets/i18n/en.json b/public/assets/i18n/en.json
index 2379c9470..bc3ff0e21 100644
--- a/public/assets/i18n/en.json
+++ b/public/assets/i18n/en.json
@@ -65,6 +65,7 @@
"SB_ERROR_DUPLICATE_INDEX": "An entry is duplicated.",
"SB_ERROR_NOT_ALLOWED": "Vous n'avez pas les autorisations pour cette opération.",
"SB_ERROR_NON_EDITABLE": "The item cannot be updated in its current state.",
+ "SB_ERROR_NON_REMOVABLE": "The item cannot be deleted in its current state.",
"SB_ERROR_CONFIG_MISSING_PARAM": "Erreur de configuration: paramètre manquant.",
"SB_ERROR_MISSING_MANDATORY": "Mandatory field.",
"SB_ERROR_MISSING_PARAM": "Missing mandatory field.",
diff --git a/public/assets/i18n/fr.json b/public/assets/i18n/fr.json
index cb6607e2c..085fc4f24 100644
--- a/public/assets/i18n/fr.json
+++ b/public/assets/i18n/fr.json
@@ -65,6 +65,7 @@
"SB_ERROR_DUPLICATE_INDEX": "Un doublon est présent.",
"SB_ERROR_NOT_ALLOWED": "Non permis. Vous n'avez pas les droits ou l'état de l'objet ne le permet pas.",
"SB_ERROR_NON_EDITABLE": "L'élement ne peut pas être modifié dans son état actuel.",
+ "SB_ERROR_NON_REMOVABLE": "L'élement ne peut pas être supprimé dans son état actuel.",
"SB_ERROR_CONFIG_MISSING_PARAM": "Paramètre(s) ou section(s) manquant(es):",
"SB_ERROR_MISSING_MANDATORY": "Champ obligatoire.",
"SB_ERROR_MISSING_PARAM": "Champ obligatoire manquant.",
diff --git a/public/console.php b/public/console.php
index ff6bd37c9..134879f48 100644
--- a/public/console.php
+++ b/public/console.php
@@ -1,4 +1,9 @@
+ Some Rights Reserved, Cedric Francoys, 2010-2023
+ Licensed under GNU LGPL 3 license
+*/
define('LOG_FILE_NAME', 'eq_error.log');
$data = '';
@@ -6,10 +11,6 @@
// get log file, using variation from URL, if any
$log_file = LOG_FILE_NAME.( (isset($_GET['f']) && strlen($_GET['f']))?('.'.$_GET['f']):'');
-if(file_exists('../log/'.$log_file)) {
- // read raw data from log file
- $data = file_get_contents('../log/'.$log_file);
-}
// retrieve logs history (variations on filename)
$log_variations = [];
@@ -31,34 +32,30 @@
unset($_GET['date']);
}
+$lines = [];
+if(file_exists('../log/'.$log_file)) {
+ // read raw data from log file
-// 1) filtering : discard lines that do not match the query
-
-$lines = explode(PHP_EOL, $data);
-$data = '';
-foreach($lines as $line) {
- if(strlen($line) <= 0) {
- continue;
- }
- if(strlen($query) > 0 && stripos($line, $query) === false) {
- continue;
+ $f = fopen('../log/'.$log_file,"r");
+ for($line = stream_get_line($f, 65535, PHP_EOL);$line!== false;$line = stream_get_line($f, 65535, PHP_EOL)) {
+ if(strlen($line) <= 0) {
+ continue;
+ }
+ if(strlen($query) > 0 && stripos($line, $query) === false) {
+ continue;
+ }
+ if(($res = json_decode($line,true)) === null) {
+ continue;
+ }
+ $lines[] = $res;
}
- $data .= $line.',';
}
-// 2) extract lines to be rendered
-
-// #memo - log file contains JSON objects separated with new line chars
-// convert notation to a valid JSON array
-$json = '['.substr($data, 0, -1).']';
-// convert JSON to a PHP associative array
-$lines = json_decode($json, true);
-
if($lines === null) {
die('Invalid JSON in log file.');
}
-$html = '
+echo '
@@ -206,7 +203,7 @@ function copy(node) {
Copied to clipboard