Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
e328e4e
fixed handling of several children with same name
cedricfrancoys Jul 19, 2023
c5f779b
fixed uppercase check for first letter
cedricfrancoys Jul 25, 2023
2c93f04
added support for values retrieved as subobject
cedricfrancoys Jul 26, 2023
2d1bdc2
routed appinfo to envinfo
cedricfrancoys Jul 26, 2023
ead450f
added visibility
cedricfrancoys Jul 26, 2023
351acf8
updated descriptions
cedricfrancoys Jul 26, 2023
62073a9
added animated demo
cedricfrancoys Jul 26, 2023
a8c1e7b
added visibilty
cedricfrancoys Jul 26, 2023
d71f283
added visibility + comments
cedricfrancoys Jul 26, 2023
8749477
syntax
cedricfrancoys Jul 26, 2023
5d506f0
added explicit namespace to announce
cedricfrancoys Jul 26, 2023
7d934ef
prevented conversion for non timestamp date/year and date/month
cedricfrancoys Jul 27, 2023
c6fa419
prevented conversion for non timestamp date/year and date/month
cedricfrancoys Jul 27, 2023
953f66a
added line for disabling opcache
cedricfrancoys Jul 27, 2023
3ce8ee3
comments & syntax
cedricfrancoys Jul 27, 2023
6a3dcdf
set as private
cedricfrancoys Jul 27, 2023
0416ca5
added settings as std app
cedricfrancoys Jul 28, 2023
88603c4
removed settings app
cedricfrancoys Jul 28, 2023
f07059a
added alias for rights identifiers
cedricfrancoys Jul 31, 2023
511a79a
added default assignment to users group
cedricfrancoys Jul 31, 2023
7866643
deleted
cedricfrancoys Jul 31, 2023
df1c600
translations
cedricfrancoys Jul 31, 2023
07983e8
fix - reverted settings menu to settings
cedricfrancoys Jul 31, 2023
a0303fa
improved for supporting both ACL and Role based access control
cedricfrancoys Jul 31, 2023
8f25c22
added hasObjectRoles + comments
cedricfrancoys Jul 31, 2023
b0ddf17
refactored getUserRights for supporting object ids collections
cedricfrancoys Jul 31, 2023
73c33f0
added (RBAC support)
cedricfrancoys Jul 31, 2023
4caa075
added support for specific object permission
cedricfrancoys Jul 31, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .docker/php.ini
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,5 @@ post_max_size = 64M
max_execution_time = 300
memory_limit = 256M
zend_extension=xdebug
; uncomment to disable opcache
; opcache.enable=0
7 changes: 7 additions & 0 deletions eq.lib.php
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,13 @@
define('QN_R_DELETE', 8);
define('QN_R_MANAGE', 16);

/* 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);

/**
* Built-in Users and Groups
*
Expand Down
233 changes: 111 additions & 122 deletions lib/equal/access/AccessController.class.php

Large diffs are not rendered by default.

14 changes: 7 additions & 7 deletions lib/equal/data/adapt/DataAdapterJson.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -118,15 +118,15 @@ public function adaptOut($value, $usage, $lang='en') {
return self::datetimeToJson($value);
case 'year':
// date/year:4 (integer 0-9999)
return intval(date('Y', $value));
return intval(($value <= 2100 && $value >= 1970)?$value:date('Y', $value));
case 'month':
// date/month (integer 1-12, ISO-8601)
return date('n', $value);
// date/weekday.mon (ISO-8601: 1 to 7, 1 is Monday)
// date/weekday.sun (0 to 6, 0 is Sunday)
// date/monthday (ISO-8601)
// date/yearweek
// date/yearday (ISO-8601)
return intval(($value <= 12 && $value >= 1)?$value:date('n', $value));
// date/weekday.mon (ISO-8601: 1 to 7, 1 is Monday)
// date/weekday.sun (0 to 6, 0 is Sunday)
// date/monthday (ISO-8601)
// date/yearweek
// date/yearday (ISO-8601)
}
break;
case 'image':
Expand Down
4 changes: 2 additions & 2 deletions lib/equal/data/adapt/DataAdapterSql.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -131,9 +131,9 @@ public function adaptOut($value, $usage, $lang='en') {
case 'plain':
return self::dateToSql($value);
case 'year':
return intval(date('Y', $value));
return intval(($value <= 2100 && $value >= 1970)?$value:date('Y', $value));
case 'month':
return date('n', $value);
return intval(($value <= 12 && $value >= 1)?$value:date('n', $value));
}
break;
case 'image':
Expand Down
11 changes: 10 additions & 1 deletion lib/equal/http/HttpMessage.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -743,7 +743,7 @@ private static function xmlToArray(\SimpleXMLElement $obj) {
$namespaces[null] = null;

foreach( $namespaces as $ns => $nsUrl ) {
// handle atributes
// handle attributes
$objAttributes = $obj->attributes($ns, true);
foreach( $objAttributes as $attributeName => $attributeValue ) {
$attribName = trim($attributeName);
Expand All @@ -759,6 +759,12 @@ private static function xmlToArray(\SimpleXMLElement $obj) {
$count_children = count($objChildren);
if($count_children) {
$has_children = true;
foreach( $objChildren as $childName => $child) {
if(!isset($map_children_names[$childName])) {
$map_children_names[$childName] = 0;
}
++$map_children_names[$childName];
}
foreach( $objChildren as $childName => $child ) {
if( !empty($ns) ) {
$childName = $ns.':'.$childName;
Expand All @@ -767,6 +773,9 @@ private static function xmlToArray(\SimpleXMLElement $obj) {
if($count_children > 1 && $res['has_children']) {
$children[] = $res;
}
elseif($map_children_names[$childName] > 1) {
$children[] = $res;
}
else {
$children[$childName] = $res;
}
Expand Down
86 changes: 51 additions & 35 deletions lib/equal/orm/Collection.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@
*/
namespace equal\orm;

use equal\data\adapt\DataAdapterProvider;

class Collection implements \Iterator, \Countable {

/** @var \equal\orm\ObjectManager */
Expand Down Expand Up @@ -77,6 +75,7 @@ public function __construct($class, $objectManager, $accessController, $authenti
$this->class = $class;
/** @var \equal\orm\ObjectManager */
$this->orm = $objectManager;
/** @var \equal\access\AccessController */
$this->ac = $accessController;
$this->am = $authenticationManager;
$this->dap = $dataAdapterProvider;
Expand Down Expand Up @@ -151,23 +150,10 @@ public function valid(): bool {
return $res;
}


public function getClass() {
return $this->class;
}

/*
public function from($offset) {
if($offset < count($this->objects)) {
$this->objects = array_slice($this->objects, $offset, null, true);
}
else {
$this->objects = [];
}
return $this;
}
*/

/**
* Pop out the n last objects of the collection (and set internal limit value)
*
Expand Down Expand Up @@ -429,7 +415,12 @@ public function grant($users_ids, $operation, $fields=[]) {
}

/**
* Feed the Collection with the IDs of the objects (of current class) that comply with the given domain.
* If no domain is given and parameter limit is left to 0, all objects are taken in account (Warning: reading such Collection might consume a large amount of memory)
*
* The domain syntax is : 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 ...]]) [ OR ...]]
*/
public function search(array $domain=[], array $params=[], $lang=null) {
// #memo - by default, we set start and limit arguments to 0 (to be ignored) because the final result set depends on User's permissions
Expand All @@ -449,6 +440,7 @@ public function search(array $domain=[], array $params=[], $lang=null) {
$params = array_merge($defaults, $params);

// 1) sanitize and validate given domain

if(!empty($domain)) {
$domain = Domain::normalize($domain);
$schema = $this->model->getSchema();
Expand All @@ -457,31 +449,52 @@ public function search(array $domain=[], array $params=[], $lang=null) {
}
}

// 2) check that current user has enough privilege to perform READ operation on given class
// #todo - extract fields names from domain, and make sure user has R_READ access on those
if(!$this->ac->isAllowed(QN_R_READ, $this->class)) {
// user has always READ access to its own objects
if(!$this->ac->isAllowed(QN_R_CREATE, $this->class)) {
// no READ nor CREATE permission: deny request
throw new \Exception($user_id.';READ;'.$this->class, QN_ERROR_NOT_ALLOWED);
}
else {
// user has CREATE access and might have created some objects: limit search to those, if any
$domain = Domain::conditionAdd($domain, ['creator', '=', $this->am->userId()]);
// 2) adapt domain according to access control type : either with ACL (permission) or Role (assignment)

if($this->orm->hasObjectRoles($this->class)) {
$roles = [];
// find all roles for which a READ permission is granted
foreach($this->class::getRoles() as $role => $descriptor) {
if(isset($descriptor['permissions'])) {
if($descriptor['permissions'] & QN_R_READ) {
$roles[] = $role;
}
}
}
// find the objects for which the user is explicitly assigned to one of these roles
$assignments_ids = $this->orm->search('core\Assignment', [ ['user_id', '=', $user_id], ['role', 'in', $roles], ['object_class', '=', $this->class] ]);
if($assignments_ids > 0 && count($assignments_ids)) {
$assignments = $this->orm->read('core\Assignment', $assignments_ids, ['object_id']);
$objects_ids = array_map(function ($a) {return $a['object_id'];}, array_values($assignments));
// limit search amongst those objects
$domain = Domain::conditionAdd($domain, ['id', 'in', $objects_ids]);
}
}
else {
// if user is not granted for READ on full class (nor parent class), neither directly nor indirectly (groups)
if(!$this->ac->hasRight($user_id, QN_R_READ, $this->class)) {
// find the objects for which the user is explicitly granted for READ
$permissions_ids = $this->orm->search('core\Permission', [ ['user_id', '=', $user_id], ['rights', '>=', QN_R_READ], ['object_class', '=', $this->class] ]);
if($permissions_ids > 0 && count($permissions_ids)) {
$permissions = $this->orm->read('core\Permission', $permissions_ids, ['object_id']);
$objects_ids = array_map(function ($a) {return $a['object_id'];}, array_values($permissions));
// limit search amongst those objects
$domain = Domain::conditionAdd($domain, ['id', 'in', $objects_ids]);
}
}
}

// 3) perform search
// we don't use the start and limit arguments here because the final result set depends on permissions

// search according to resulting domain and specific params, if given
$ids = $this->orm->search($this->class, $domain, $params['sort'], $params['start'], $params['limit'], $lang);

// $ids is an error code
if($ids < 0) {
throw new \Exception(Domain::toString($domain), $ids);
}
if(count($ids)) {
// filter results using access controller (reduce resulting ids based on access rights of current user)
$ids = $this->ac->filter(QN_R_READ, $this->class, [], $ids);
// init keys of 'objects' member (so far, it is an empty array)
// init keys of `objects` member (so far, it is an empty array)
$this->ids($ids);
}
else {
Expand All @@ -492,7 +505,7 @@ public function search(array $domain=[], array $params=[], $lang=null) {
}

/**
* Creates new instances by copying exiting ones
* Create new instances by cloning the ones present in current Collection.
*
* @return Collection Returns the current Collection.
* @example $newObject = MyClass::id(5)->clone()->first();
Expand Down Expand Up @@ -747,7 +760,7 @@ public function read($fields, $lang=null) {
/** @var Collection */
$children = $target['foreign_object']::ids($this->objects[$id][$field])->read($subfields, ($lang)?$lang:$this->lang);
if($target_type == 'many2one') {
// #memo - result might be null or contain sub-collections
// #memo - result might be null or an Object (that might contain sub-collections)
$this->objects[$id][$field] = $children->first();
}
else {
Expand Down Expand Up @@ -814,6 +827,9 @@ public function update(array $values, $lang=null) {
return $this;
}

/**
* @return Collection returns the current instance (allowing calls chaining)
*/
public function delete($permanent=false) {
if(count($this->objects)) {
$user_id = $this->am->userId();
Expand Down Expand Up @@ -851,6 +867,8 @@ public function delete($permanent=false) {
* Attempts to perform a specific action to a series of objects.
* If no workflow is defined, the call is ignored (no action is taken).
* If there is no match or if there are some conditions on the transition that are not met, it returns an error code.
*
* @return Collection returns the current instance (allowing calls chaining)
*/
public function transition($transition) {
// retrieve targeted identifiers
Expand All @@ -868,9 +886,7 @@ public function transition($transition) {
* If there is no match or if there are some conditions on the transition that are not met, it returns an error code.
*
* @param string $action Name of the requested action.
*
* @return array Returns an associative array containing invalid fields with their associated error_message_id.
* An empty array means all fields are valid. In case of error, the method returns a negative integer.
* @return Collection returns the current instance (allowing calls chaining)
*/
public function do($action) {
// check if action can be performed
Expand Down
52 changes: 42 additions & 10 deletions lib/equal/orm/ObjectManager.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,10 @@ public static function constants() {
return ['DEFAULT_LANG', 'UPLOAD_MAX_FILE_SIZE', 'FILE_STORAGE_MODE', 'DRAFT_VALIDITY'];
}

/**
* #todo - deprecated : use the DBConnection service instead
* @deprecated
*/
public function getDB() {
return $this->db;
}
Expand Down Expand Up @@ -289,7 +293,6 @@ private function getStaticInstance($class, $fields=[]) {
return $this->models[$class];
}


/**
* Gets the name of the table associated to the specified class (required to convert namespace notation).
*
Expand Down Expand Up @@ -338,12 +341,21 @@ public static function getObjectRootClass($class) {
return $entity;
}

/**
* Retrieve the root parent class of a class.
* If there are several level of inheritance, the method loops up until the first class that inherits from the Model interface (`equal\orm\Model`).
*
* @param string $class The full name of the entity with its namespace.
* @return array Returns an array containing the list of all the classes the given class inherits from (order is not guaranteed).
*/
public static function getObjectParentsClasses($class) {
$classes = [];
$entity = $class;
while(true) {
$parent = get_parent_class($entity);
if(!$parent || $parent == 'equal\orm\Model') break;
if(!$parent || $parent == 'equal\orm\Model') {
break;
}
$classes[] = $parent;
$entity = $parent;
}
Expand All @@ -353,11 +365,11 @@ public static function getObjectParentsClasses($class) {
/**
* Retrieve the package in which is defined the class of an object (required to convert namespace notation).
*
* @param string $object_class The full name of the entity with its namespace.
* @param string $class The full name of the entity with its namespace.
* @return string
*/
public static function getObjectPackage($object_class) {
$parts = explode('\\', $object_class);
public static function getObjectPackage($class) {
$parts = explode('\\', $class);
return array_shift($parts);
}

Expand All @@ -366,15 +378,35 @@ public static function getObjectPackage($object_class) {
* note: this method is not set as static since we need to load class file in order to retrieve the schema
* (and this is only done in the getStaticInstance method)
*
* @param string $object_class The full name of the entity with its namespace.
* @param string $class The full name of the entity with its namespace.
* @return array
*/
public function getObjectSchema($object_class) {
$object = $this->getStaticInstance($object_class);
public function getObjectSchema($class) {
$object = $this->getStaticInstance($class);
return $object->getSchema();
}


/**
* Check if a getRoles() method is defined on the targeted class.
* Methods from ancestors classes are not taken in account.
*
* @param string $class The full name of the entity with its namespace.
* @return array|boolean Returns either and associative array mapping roles with their descriptors. In case no getRoles() method is defined, returns false.
*/
public function hasObjectRoles($class) {
if(method_exists($class, 'getRoles')) {
/** @var \ReflectionClass */
$reflectionClass = new \ReflectionClass($class);
return ($reflectionClass->getMethod('getRoles')->class == $class);
}
return false;
}

/**
* #todo - deprecate : use Field class instead.
* @deprecated
*/
public function getFinalType($object_class, $field) {
$schema = $this->getObjectSchema($object_class);

Expand All @@ -401,7 +433,7 @@ public function getFinalType($object_class, $field) {
* @param array $check_array
* @param array $schema
* @param string $field
* @return bool Returns true if all attributes in $chek_array actually exist in the given schema.
* @return bool Returns true if all attributes in $check_array actually exist in the given schema.
*/
public static function checkFieldAttributes($check_array, $schema, $field) {
if (!isset($schema) || !isset($schema[$field])) {
Expand Down Expand Up @@ -2254,7 +2286,7 @@ public function transition($class, $ids, $transition) {
}

/**
* Searches for the objects that comply with the domain (series of criteria).
* Search for the objects that comply with the domain (series of criteria).
*
* The domain syntax is : 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
Expand Down
9 changes: 9 additions & 0 deletions packages/core/actions/model/onchange.php
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,15 @@

// adapt resulting values to json
foreach($result as $field => $value) {
// leave array untouched (not an ORM type)
if(is_array($value)) {
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;
}
$f = new Field($schema[$field]);
// adapt received values based on their type (as defined in schema)
$result[$field] = $adapter->adaptOut($value, $f->getUsage());
Expand Down
Loading