3 /* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
6 * Fields config handling
10 * Copyright © 2009-2020 The Galette Team
12 * This file is part of Galette (http://galette.tuxfamily.org).
14 * Galette is free software: you can redistribute it and/or modify
15 * it under the terms of the GNU General Public License as published by
16 * the Free Software Foundation, either version 3 of the License, or
17 * (at your option) any later version.
19 * Galette is distributed in the hope that it will be useful,
20 * but WITHOUT ANY WARRANTY; without even the implied warranty of
21 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 * GNU General Public License for more details.
24 * You should have received a copy of the GNU General Public License
25 * along with Galette. If not, see <http://www.gnu.org/licenses/>.
30 * @author Johan Cwiklinski <johan@x-tnd.be>
31 * @copyright 2009-2020 The Galette Team
32 * @license http://www.gnu.org/licenses/gpl-3.0.html GPL License 3.0 or (at your option) any later version
33 * @link http://galette.tuxfamily.org
34 * @since Available since 0.7dev - 2009-03-26
37 namespace Galette\Entity
;
42 use Laminas\Db\Adapter\Adapter
;
44 use Galette\Core\Login
;
45 use Galette\Core\Authentication
;
48 * Fields config class for galette :
49 * defines fields visibility for lists and forms
50 * defines fields order and requirement flag for forms
55 * @author Johan Cwiklinski <johan@x-tnd.be>
56 * @copyright 2009-2020 The Galette Team
57 * @license http://www.gnu.org/licenses/gpl-3.0.html GPL License 3.0 or (at your option) any later version
58 * @link http://galette.tuxfamily.org
59 * @since Available since 0.7dev - 2009-03-26
63 public const NOBODY
= 0;
64 public const USER_WRITE
= 1;
65 public const ADMIN
= 2;
66 public const STAFF
= 3;
67 public const MANAGER
= 4;
68 public const USER_READ
= 5;
70 public const TYPE_STR
= 0;
71 public const TYPE_HIDDEN
= 1;
72 public const TYPE_BOOL
= 2;
73 public const TYPE_INT
= 3;
74 public const TYPE_DEC
= 4;
75 public const TYPE_DATE
= 5;
76 public const TYPE_TXT
= 6;
77 public const TYPE_PASS
= 7;
78 public const TYPE_EMAIL
= 8;
79 public const TYPE_URL
= 9;
80 public const TYPE_RADIO
= 10;
81 public const TYPE_SELECT
= 11;
84 protected $core_db_fields = array();
85 protected $all_required = array();
86 protected $all_visibles = array();
87 protected $categorized_fields = array();
89 protected $defaults = null;
90 protected $cats_defaults = null;
92 private $staff_fields = array(
99 private $admin_fields = array(
103 public const TABLE
= 'fields_config';
106 * Fields that are not visible in the
107 * form should not be visible here.
109 private $non_required = array(
118 //Fields we do not want to be set as required
126 private $non_form_elements = array(
131 private $non_display_elements = array(
141 * Default constructor
143 * @param Db $zdb Database
144 * @param string $table the table for which to get fields configuration
145 * @param array $defaults default values
146 * @param array $cats_defaults default categories values
147 * @param boolean $install Are we calling from installer?
149 public function __construct(Db
$zdb, string $table, array $defaults, array $cats_defaults, bool $install = false)
152 $this->table
= $table;
153 $this->defaults
= $defaults;
154 $this->cats_defaults
= $cats_defaults;
155 //prevent check at install time...
158 $this->checkUpdate();
163 * Load current fields configuration from database.
167 public function load()
170 $select = $this->zdb
->select(self
::TABLE
);
172 ->where(array('table_name' => $this->table
))
173 ->order(array(FieldsCategories
::PK
, 'position ASC'));
175 $results = $this->zdb
->execute($select);
176 $this->core_db_fields
= [];
178 foreach ($results as $k) {
179 $field = $this->buildField($k);
180 $this->core_db_fields
[$k->field_id
] = $field;
185 } catch (Throwable
$e) {
187 'Fields configuration cannot be loaded!',
195 * Prepare a field (required data, automation)
197 * @param ArrayObject $rset DB ResultSet row
199 * @return ArrayObject
201 protected function prepareField(ArrayObject
$rset): ArrayObject
203 if ($rset->field_id
=== 'parent_id') {
204 $rset->readonly
= true;
205 $rset->required
= false;
211 * Prepare a field (required data, automation)
213 * @param ArrayObject $rset DB ResultSet row
217 protected function buildField(ArrayObject
$rset): array
219 $rset = $this->prepareField($rset);
221 'field_id' => $rset->field_id
,
222 'label' => $this->defaults
[$rset->field_id
]['label'],
223 'category' => (int)$rset->id_field_category
,
224 'visible' => (int)$rset->visible
,
225 'required' => (bool)$rset->required
,
226 'propname' => $this->defaults
[$rset->field_id
]['propname'],
227 'position' => (int)$rset->position
,
234 * Create field array configuration,
235 * Several lists of fields are kept (visible, requireds, etc), build them.
239 protected function buildLists()
242 $this->categorized_fields
= [];
243 $this->all_required
= [];
244 $this->all_visibles
= [];
246 foreach ($this->core_db_fields
as $field) {
247 $this->addToLists($field);
252 * Adds a field to lists
254 * @param array $field Field values
258 protected function addToLists(array $field)
260 if ($field['position'] >= 0) {
261 $this->categorized_fields
[$field['category']][] = $field;
264 //array of all required fields
265 if ($field['required']) {
266 $this->all_required
[$field['field_id']] = $field['required'];
269 //array of all fields visibility
270 $this->all_visibles
[$field['field_id']] = $field['visible'];
274 * Is a field set as required?
276 * @param string $field Field name
280 public function isRequired($field)
282 return isset($this->all_required
[$field]);
286 * Temporary set a field as not required
287 * (password for existing members for example)
289 * @param string $field Field name
293 public function setNotRequired($field)
295 if (isset($this->all_required
[$field])) {
296 unset($this->all_required
[$field]);
299 foreach ($this->categorized_fields
as &$cat) {
300 foreach ($cat as &$f) {
301 if ($f['field_id'] === $field) {
302 $f['required'] = false;
310 * Checks if all fields are present in the database.
312 * For now, this function only checks if count matches.
316 private function checkUpdate()
318 $class = get_class($this);
321 $_all_fields = array();
322 if (count($this->core_db_fields
)) {
324 $this->core_db_fields
,
325 function ($field) use (&$_all_fields) {
326 $_all_fields[$field['field_id']] = $field;
330 //hum... no records. Let's check if any category exists
331 $select = $this->zdb
->select(FieldsCategories
::TABLE
);
332 $results = $this->zdb
->execute($select);
334 if ($results->count() == 0) {
335 //categories are missing, add them
336 $categories = new FieldsCategories($this->zdb
, $this->cats_defaults
);
337 $categories->installInit();
341 if (count($this->defaults
) != count($_all_fields)) {
343 'Fields configuration count for `' . $this->table
.
344 '` columns does not match records. Is : ' .
345 count($_all_fields) . ' and should be ' .
346 count($this->defaults
),
351 foreach ($this->defaults
as $k => $f) {
352 if (!isset($_all_fields[$k])) {
354 'Missing field configuration for field `' . $k . '`',
359 'table_name' => $this->table
,
360 'required' => $f['required'],
361 'visible' => $f['visible'],
362 'position' => $f['position'],
363 'category' => $f['category'],
364 'list_visible' => $f['list_visible'] ??
false,
365 'list_position' => $f['list_position'] ??
null
370 if (count($params) > 0) {
371 $this->insert($params);
375 } catch (Throwable
$e) {
377 '[' . $class . '] An error occurred while checking update for ' .
378 'fields configuration for table `' . $this->table
. '`. ' .
387 * Set default fields configuration at install time. All previous
388 * existing values will be dropped first, including fields categories.
390 * @return boolean|\Exception
392 public function installInit()
395 $fields = array_keys($this->defaults
);
396 $categories = new FieldsCategories($this->zdb
, $this->cats_defaults
);
398 //first, we drop all values
399 $delete = $this->zdb
->delete(self
::TABLE
);
401 array('table_name' => $this->table
)
403 $this->zdb
->execute($delete);
404 //take care of fields categories, for db relations
405 $categories->installInit($this->zdb
);
407 $fields = array_keys($this->defaults
);
408 foreach ($fields as $f) {
409 //build default config for each field
412 'table_name' => $this->table
,
413 'required' => $this->defaults
[$f]['required'],
414 'visible' => $this->defaults
[$f]['visible'],
415 'position' => (int)$this->defaults
[$f]['position'],
416 'category' => $this->defaults
[$f]['category'],
417 'list_visible' => $this->defaults
[$f]['list_visible'] ??
false,
418 'list_position' => $this->defaults
[$f]['list_position'] ??
-1
421 $this->insert($params);
424 'Default fields configuration were successfully stored.',
428 } catch (Throwable
$e) {
430 'Unable to initialize default fields configuration.' . $e->getMessage(),
439 * Get non required fields
443 public function getNonRequired()
445 return $this->non_required
;
449 * Retrieve form elements
451 * @param Login $login Login instance
452 * @param boolean $new True when adding a new member
453 * @param boolean $selfs True if we're called from self subscription page
457 public function getFormElements(Login
$login, $new, $selfs = false)
461 $hidden_elements = [];
464 //get columns descriptions
465 $columns = $this->zdb
->getColumns($this->table
);
467 $access_level = $login->getAccessLevel();
468 $categories = FieldsCategories
::getList($this->zdb
);
470 foreach ($categories as $c) {
471 $cpk = FieldsCategories
::PK
;
473 foreach ($this->cats_defaults
as $conf_cat) {
474 if ($conf_cat['id'] == $c->$cpk) {
475 $cat_label = $conf_cat['category'];
479 if ($cat_label === null) {
480 $cat_label = $c->category
;
482 $cat = (object)array(
483 'id' => (int)$c->$cpk,
484 'label' => $cat_label,
485 'elements' => array()
488 $elements = $this->categorized_fields
[$c->$cpk];
489 $cat->elements
= array();
491 foreach ($elements as $elt) {
493 $o->readonly
= false;
495 if ($o->field_id
== 'id_adh') {
496 // ignore access control, as member ID is always needed
497 if (!$preferences->pref_show_id ||
$new === true) {
498 $hidden_elements[] = $o;
500 $o->type
= self
::TYPE_STR
;
502 $cat->elements
[$o->field_id
] = $o;
504 } elseif ($o->field_id
== 'parent_id') {
505 $hidden_elements[] = $o;
507 // skip fields blacklisted for edition
509 in_array($o->field_id
, $this->non_form_elements
)
510 ||
$selfs && $this->isSelfExcluded($o->field_id
)
515 // skip fields according to access control
517 $o->visible
== self
::NOBODY ||
518 ($o->visible
== self
::ADMIN
&&
519 $access_level < Authentication
::ACCESS_ADMIN
) ||
520 ($o->visible
== self
::STAFF
&&
521 $access_level < Authentication
::ACCESS_STAFF
) ||
522 ($o->visible
== self
::MANAGER
&&
523 $access_level < Authentication
::ACCESS_MANAGER
)
528 if (preg_match('/date/', $o->field_id
)) {
529 $o->type
= self
::TYPE_DATE
;
530 } elseif (preg_match('/bool/', $o->field_id
)) {
531 $o->type
= self
::TYPE_BOOL
;
533 $o->field_id
== 'titre_adh'
534 ||
$o->field_id
== 'pref_lang'
535 ||
$o->field_id
== 'id_statut'
537 $o->type
= self
::TYPE_SELECT
;
538 } elseif ($o->field_id
== 'sexe_adh') {
539 $o->type
= self
::TYPE_RADIO
;
541 $o->type
= self
::TYPE_STR
;
544 //retrieve field information from DB
545 foreach ($columns as $column) {
546 if ($column->getName() === $o->field_id
) {
548 = $column->getCharacterMaximumLength();
549 $o->default = $column->getColumnDefault();
550 $o->datatype
= $column->getDataType();
555 // disabled field according to access control
557 $o->visible
== self
::USER_READ
&&
558 $access_level == Authentication
::ACCESS_USER
562 $o->disabled
= false;
565 if ($selfs === true) {
566 //email, login and password are always required for self subscription
567 $srequireds = ['email_adh', 'login_adh'];
568 if (in_array($o->field_id
, $srequireds)) {
572 $cat->elements
[$o->field_id
] = $o;
576 if (count($cat->elements
) > 0) {
577 $form_elements[] = $cat;
581 'fieldsets' => $form_elements,
582 'hiddens' => $hidden_elements
584 } catch (Throwable
$e) {
586 'An error occurred getting form elements',
594 * Retrieve display elements
596 * @param Login $login Login instance
600 public function getDisplayElements(Login
$login)
604 $display_elements = [];
605 $access_level = $login->getAccessLevel();
606 $categories = FieldsCategories
::getList($this->zdb
);
608 foreach ($categories as $c) {
609 $cpk = FieldsCategories
::PK
;
611 foreach ($this->cats_defaults
as $conf_cat) {
612 if ($conf_cat['id'] == $c->$cpk) {
613 $cat_label = $conf_cat['category'];
617 if ($cat_label === null) {
618 $cat_label = $c->category
;
620 $cat = (object)array(
621 'id' => (int)$c->$cpk,
622 'label' => $cat_label,
623 'elements' => array()
626 $elements = $this->categorized_fields
[$c->$cpk];
627 $cat->elements
= array();
629 foreach ($elements as $elt) {
632 if ($o->field_id
== 'id_adh') {
633 // ignore access control, as member ID is always needed
634 if (!isset($preferences) ||
!$preferences->pref_show_id
) {
635 $hidden_elements[] = $o;
637 $o->type
= self
::TYPE_STR
;
638 $cat->elements
[$o->field_id
] = $o;
641 // skip fields blacklisted for display
642 if (in_array($o->field_id
, $this->non_display_elements
)) {
646 // skip fields according to access control
648 $o->visible
== self
::NOBODY ||
649 ($o->visible
== self
::ADMIN
&&
650 $access_level < Authentication
::ACCESS_ADMIN
) ||
651 ($o->visible
== self
::STAFF
&&
652 $access_level < Authentication
::ACCESS_STAFF
) ||
653 ($o->visible
== self
::MANAGER
&&
654 $access_level < Authentication
::ACCESS_MANAGER
)
659 $cat->elements
[$o->field_id
] = $o;
663 if (count($cat->elements
) > 0) {
664 $display_elements[] = $cat;
667 return $display_elements;
668 } catch (Throwable
$e) {
670 'An error occurred getting display elements',
678 * Get required fields
680 * @return array of all required fields. Field names = keys
682 public function getRequired()
684 return $this->all_required
;
690 * @return array of all visibles fields
692 public function getVisibilities()
694 return $this->all_visibles
;
698 * Get visibility for specified field
700 * @param string $field The requested field
704 public function getVisibility($field)
706 return $this->all_visibles
[$field];
710 * Get all fields with their categories
714 public function getCategorizedFields()
716 return $this->categorized_fields
;
722 * @param array $fields categorized fields array
726 public function setFields($fields)
728 $this->categorized_fields
= $fields;
729 return $this->store();
733 * Store config in database
737 private function store()
739 $class = get_class($this);
742 $this->zdb
->connection
->beginTransaction();
744 $update = $this->zdb
->update(self
::TABLE
);
747 'required' => ':required',
748 'visible' => ':visible',
749 'position' => ':position',
750 FieldsCategories
::PK
=> ':' . FieldsCategories
::PK
754 'field_id' => ':field_id',
755 'table_name' => $this->table
758 $stmt = $this->zdb
->sql
->prepareStatementForSqlObject($update);
761 foreach ($this->categorized_fields
as $cat) {
762 foreach ($cat as $pos => $field) {
763 if (in_array($field['field_id'], $this->non_required
)) {
764 $field['required'] = $this->zdb
->isPostgres() ?
'false' : 0;
767 if ($field['field_id'] === 'parent_id') {
768 $field['visible'] = 0;
772 'required' => $field['required'],
773 'visible' => $field['visible'],
775 FieldsCategories
::PK
=> $field['category'],
776 'field_id' => $field['field_id']
779 $stmt->execute($params);
784 '[' . $class . '] Fields configuration stored successfully! ',
791 '[' . $class . '] Fields configuration for table %s stored ' .
797 $this->zdb
->connection
->commit();
798 return $this->load();
799 } catch (Throwable
$e) {
800 $this->zdb
->connection
->rollBack();
802 '[' . $class . '] An error occurred while storing fields ' .
803 'configuration for table `' . $this->table
. '`.' .
808 $e->getTraceAsString(),
816 * Migrate old required fields configuration
817 * Only needeed for 0.7.4 upgrade
818 * (should have been 0.7.3 - but I missed that.)
822 public function migrateRequired()
824 $old_required = null;
827 $select = $this->zdb
->select('required');
828 $select->from(PREFIX_DB
. 'required');
830 $old_required = $this->zdb
->execute($select);
831 } catch (\Exception
$pe) {
833 'Unable to retrieve required fields_config. Maybe ' .
834 'the table does not exists?',
841 $this->zdb
->connection
->beginTransaction();
843 $update = $this->zdb
->update(self
::TABLE
);
846 'required' => ':required'
850 'field_id' => ':field_id',
851 'table_name' => $this->table
855 $stmt = $this->zdb
->sql
->prepareStatementForSqlObject($update);
857 foreach ($old_required as $or) {
860 'required' => ($or->required
=== false) ?
861 ($this->zdb
->isPostgres() ?
'false' : 0) : true,
862 'field_id' => $or->field_id
867 $class = get_class($this);
872 '[' . $class . '] Required fields for table %s upgraded ' .
878 $this->zdb
->db
->query(
879 'DROP TABLE ' . PREFIX_DB
. 'required',
880 Adapter
::QUERY_MODE_EXECUTE
883 $this->zdb
->connection
->commit();
885 } catch (Throwable
$e) {
886 $this->zdb
->connection
->rollBack();
888 'An error occurred migrating old required fields. | ' .
897 * Insert values in database
899 * @param array $values Values to insert
903 private function insert($values)
905 $insert = $this->zdb
->insert(self
::TABLE
);
908 'field_id' => ':field_id',
909 'table_name' => ':table_name',
910 'required' => ':required',
911 'visible' => ':visible',
912 FieldsCategories
::PK
=> ':category',
913 'position' => ':position',
914 'list_visible' => ':list_visible',
915 'list_position' => ':list_position'
918 $stmt = $this->zdb
->sql
->prepareStatementForSqlObject($insert);
919 foreach ($values as $d) {
920 $required = $d['required'];
921 if ($required === false) {
922 $required = $this->zdb
->isPostgres() ?
'false' : 0;
925 $list_visible = $f['list_visible'] ??
false;
926 if ($list_visible === false) {
927 $list_visible = $this->zdb
->isPostgres() ?
'false' : 0;
932 'field_id' => $d['field_id'],
933 'table_name' => $d['table_name'],
934 'required' => $required,
935 'visible' => $d['visible'],
936 'category' => $d['category'],
937 'position' => $d['position'],
938 'list_visible' => $list_visible,
939 'list_position' => $d['list_position'] ??
-1
946 * Does field should be displayed in self subscription page
948 * @param string $name Field name
952 public function isSelfExcluded($name)
964 * Get fields for massive changes
965 * @see FieldsConfig::getFormElements
967 * @param array $fields Member fields
968 * @param Login $login Login instance
972 public function getMassiveFormElements(array $fields, Login
$login)
974 $visibles = $this->getVisibilities();
975 $access_level = $login->getAccessLevel();
977 //remove not searchable fields
978 unset($fields['mdp_adh']);
980 foreach ($fields as $k => $f) {
982 $visibles[$k] == FieldsConfig
::NOBODY ||
983 ($visibles[$k] == FieldsConfig
::ADMIN
&&
984 $access_level < Authentication
::ACCESS_ADMIN
) ||
985 ($visibles[$k] == FieldsConfig
::STAFF
&&
986 $access_level < Authentication
::ACCESS_STAFF
) ||
987 ($visibles[$k] == FieldsConfig
::MANAGER
&&
988 $access_level < Authentication
::ACCESS_MANAGER
)
1001 'bool_display_info',
1007 $mass_fields = array_intersect(array_keys($fields), $mass_fields);
1009 foreach ($mass_fields as $mass_field) {
1010 $this->setNotRequired($mass_field);
1012 $form_elements = $this->getFormElements($login, false);
1013 unset($form_elements['hiddens']);
1015 foreach ($form_elements['fieldsets'] as &$form_element) {
1016 $form_element->elements
= array_intersect_key($form_element->elements
, array_flip($mass_fields));
1018 return $form_elements;
1022 * Get field configuration
1024 * @param string $name Field name
1028 public function getField($name): array
1030 if (!isset($this->core_db_fields
[$name])) {
1031 throw new \
UnexpectedValueException("$name fied does not exists");
1033 return $this->core_db_fields
[$name];