]> git.agnieray.net Git - galette.git/blob - galette/lib/Galette/Features/Dynamics.php
Improve coding standards
[galette.git] / galette / lib / Galette / Features / Dynamics.php
1 <?php
2
3 /**
4 * Copyright © 2003-2024 The Galette Team
5 *
6 * This file is part of Galette (https://galette.eu).
7 *
8 * Galette is free software: you can redistribute it and/or modify
9 * it under the terms of the GNU General Public License as published by
10 * the Free Software Foundation, either version 3 of the License, or
11 * (at your option) any later version.
12 *
13 * Galette is distributed in the hope that it will be useful,
14 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 * GNU General Public License for more details.
17 *
18 * You should have received a copy of the GNU General Public License
19 * along with Galette. If not, see <http://www.gnu.org/licenses/>.
20 */
21
22 namespace Galette\Features;
23
24 use Galette\Core\Login;
25 use Galette\Entity\Adherent;
26 use Galette\Repository\DynamicFieldsSet;
27 use Throwable;
28 use Analog\Analog;
29 use Galette\DynamicFields\File;
30 use Galette\DynamicFields\Date;
31 use Galette\DynamicFields\Boolean;
32 use Galette\Entity\DynamicFieldsHandle;
33
34 /**
35 * Dynamics fields trait
36 *
37 * @author Johan Cwiklinski <johan@x-tnd.be>
38 */
39
40 trait Dynamics
41 {
42 use Dependencies;
43
44 protected string $name_pattern = 'info_field_';
45
46 protected DynamicFieldsHandle $dynamics;
47
48 /**
49 * Load dynamic fields for member
50 *
51 * @return void
52 */
53 private function loadDynamicFields(): void
54 {
55 /** @phpstan-ignore-next-line */
56 if (property_exists($this, 'login') && ($this->login ?? null) instanceof Login) {
57 $login = $this->login;
58 } else {
59 global $login;
60 }
61 $this->dynamics = new DynamicFieldsHandle($this->zdb, $login, $this);
62 }
63
64 /**
65 * Get dynamic fields
66 *
67 * @return DynamicFieldsHandle
68 */
69 public function getDynamicFields(): DynamicFieldsHandle
70 {
71 if (empty($this->dynamics)) {
72 $this->loadDynamicFields();
73 }
74 return $this->dynamics;
75 }
76
77 /**
78 * Extract posted values for dynamic fields
79 *
80 * @param array<string, mixed> $post Posted values
81 * @param array<string,int|bool> $required Array of required fields
82 * @param array<string> $disabled Array of disabled fields
83 *
84 * @return bool
85 */
86 protected function dynamicsCheck(array $post, array $required, array $disabled): bool
87 {
88 if (!isset($this->dynamics)) {
89 Analog::log(
90 'Dynamics fields have not been loaded, cannot be checked. (from: ' . __METHOD__ . ')',
91 Analog::WARNING
92 );
93 $this->loadDynamicFields();
94 }
95
96 if ($post != null) {
97 $valid = true;
98 $fields = $this->dynamics->getFields();
99
100 $dynamic_fields = [];
101 //posted fields
102 foreach ($post as $key => $value) {
103 // if the field is enabled, and match patterns check it
104 if (isset($disabled[$key]) || substr($key, 0, 11) != $this->name_pattern) {
105 continue;
106 }
107
108 list($field_id, $val_index) = explode('_', str_replace($this->name_pattern, '', $key));
109 if (!is_numeric($field_id) || !is_numeric($val_index)) {
110 continue;
111 }
112
113 $dynamic_fields[$key] = [
114 'value' => $value,
115 'field_id' => $field_id,
116 'val_index' => $val_index
117 ];
118 }
119
120 //some fields may be missing in posted values (checkboxes)
121 foreach ($fields as $field) {
122 $pattern = '/' . $this->name_pattern . $field->getId() . '_(\d)/';
123 if ($field instanceof Boolean && !preg_grep($pattern, array_keys($dynamic_fields))) {
124 $dynamic_fields[$this->name_pattern . $field->getId() . '_1'] = [
125 'value' => '',
126 'field_id' => $field->getId(),
127 'val_index' => 1
128 ];
129 }
130 }
131
132 foreach ($dynamic_fields as $key => $dfield_values) {
133 $field_id = $dfield_values['field_id'];
134 $value = $dfield_values['value'];
135 $val_index = $dfield_values['val_index'];
136
137 if ($fields[$field_id]->isRequired() && (trim($value) === '' || $value == null)) {
138 $this->errors[] = str_replace(
139 '%field',
140 $fields[$field_id]->getName(),
141 _T('Missing required field %field')
142 );
143 } else {
144 if ($fields[$field_id] instanceof File) {
145 //delete checkbox
146 $filename = $fields[$field_id]->getFileName($this->id, $val_index);
147 if (file_exists(GALETTE_FILES_PATH . $filename)) {
148 unlink(GALETTE_FILES_PATH . $filename);
149 } elseif (!$this instanceof Adherent) {
150 $test_filename = $fields[$field_id]->getFileName($this->id, $val_index, 'member');
151 if (file_exists(GALETTE_FILES_PATH . $test_filename)) {
152 unlink(GALETTE_FILES_PATH . $test_filename);
153 }
154 }
155 $this->dynamics->setValue($this->id, $field_id, $val_index, '');
156 } else {
157 if ($fields[$field_id] instanceof Date && !empty(trim($value))) {
158 //check date format
159 try {
160 $d = \DateTime::createFromFormat(__("Y-m-d"), $value);
161 if ($d === false) {
162 //try with non localized date
163 $d = \DateTime::createFromFormat("Y-m-d", $value);
164 if ($d === false) {
165 throw new \Exception('Incorrect format');
166 }
167 }
168 } catch (Throwable $e) {
169 $valid = false;
170 Analog::log(
171 'Wrong date format. field: ' . $field_id .
172 ', value: ' . $value . ', expected fmt: ' .
173 __("Y-m-d") . ' | ' . $e->getMessage(),
174 Analog::INFO
175 );
176 $this->errors[] = sprintf(
177 //TRANS: %1$s date format, %2$s is the field name
178 _T('- Wrong date format (%1$s) for %2$s!'),
179 __("Y-m-d"),
180 $fields[$field_id]->getName()
181 );
182 }
183 }
184 //actual field value
185 if ($value !== null && trim($value) !== '') {
186 $this->dynamics->setValue($this->id ?? null, $field_id, $val_index, $value);
187 } else {
188 $this->dynamics->unsetValue($field_id, $val_index);
189 }
190 }
191 }
192 }
193
194 return $valid;
195 }
196 return false;
197 }
198
199 /**
200 * Stores dynamic fields
201 *
202 * @param bool $transaction True if a transaction already exists
203 *
204 * @return bool
205 */
206 protected function dynamicsStore(bool $transaction = false): bool
207 {
208 if (!isset($this->dynamics)) {
209 Analog::log(
210 'Dynamics fields have not been loaded, cannot be stored. (from: ' . __METHOD__ . ')',
211 Analog::WARNING
212 );
213 $this->loadDynamicFields();
214 }
215 $return = $this->dynamics->storeValues($this->id, $transaction);
216 if (method_exists($this, 'updateModificationDate') && $this->dynamics->hasChanged()) {
217 $this->updateModificationDate();
218 }
219 return $return;
220 }
221
222 /**
223 * Store dynamic Files
224 *
225 * @param array<string, mixed> $files Posted files
226 *
227 * @return void
228 */
229 protected function dynamicsFiles(array $files): void
230 {
231 $this->loadDynamicFields();
232 $fields = $this->dynamics->getFields();
233 $store = false;
234
235 foreach ($files as $key => $file) {
236 if (substr($key, 0, 11) != $this->name_pattern) {
237 continue;
238 }
239
240 list($field_id, $val_index) = explode('_', str_replace($this->name_pattern, '', $key));
241 if (!is_numeric($field_id) || !is_numeric($val_index)) {
242 continue;
243 }
244
245 if (
246 $file['error'] == UPLOAD_ERR_NO_FILE
247 && $file['name'] == ''
248 && $file['tmp_name'] == ''
249 ) {
250 //not upload atempt.
251 continue;
252 } elseif ($file['error'] !== UPLOAD_ERR_OK) {
253 Analog::log("file upload error", Analog::ERROR);
254 continue;
255 }
256
257 $tmp_filename = $file['tmp_name'];
258 if ($tmp_filename == '') {
259 Analog::log("empty temporary filename", Analog::ERROR);
260 continue;
261 }
262
263 if (!is_uploaded_file($tmp_filename)) {
264 Analog::log("not an uploaded file", Analog::ERROR);
265 continue;
266 }
267
268 $max_size =
269 $fields[$field_id]->getSize() ?
270 $fields[$field_id]->getSize() * 1024 : File::DEFAULT_MAX_FILE_SIZE * 1024;
271 if ($file['size'] > $max_size) {
272 Analog::log(
273 "file too large: " . $file['size'] . " Ko, vs $max_size Ko allowed",
274 Analog::ERROR
275 );
276 $this->errors[] = preg_replace(
277 '|%d|',
278 (string)$max_size,
279 _T("File is too big. Maximum allowed size is %dKo")
280 );
281 continue;
282 }
283
284 $form_name = $this->getFormName();
285 if ($form_name === 'adh') {
286 $form_name = 'member'; //for compatibility with existing files
287 }
288 $new_filename = sprintf(
289 '%s_%d_field_%d_value_%d',
290 $form_name,
291 $this->id,
292 $field_id,
293 $val_index
294 );
295 Analog::log("new file: $new_filename", Analog::DEBUG);
296
297 move_uploaded_file(
298 $tmp_filename,
299 GALETTE_FILES_PATH . $new_filename
300 );
301 $this->dynamics->setValue($this->id, (int)$field_id, (int)$val_index, $file['name']);
302 $store = true;
303 }
304
305 if ($store === true) {
306 $this->dynamicsStore();
307 }
308 }
309
310 /**
311 * Remove dynamic fields values
312 *
313 * @param bool $transaction True if a transaction already exists
314 *
315 * @return bool
316 */
317 protected function dynamicsRemove(bool $transaction = false): bool
318 {
319 if (!isset($this->dynamics)) {
320 Analog::log(
321 'Dynamics fields have not been loaded, cannot be removed. (from: ' . __METHOD__ . ')',
322 Analog::WARNING
323 );
324 $this->loadDynamicFields();
325 }
326 $return = $this->dynamics->removeValues($this->id, $transaction);
327 return $return;
328 }
329
330 /**
331 * Get errors
332 *
333 * @return array<string>
334 */
335 public function getErrors(): array
336 {
337 return $this->errors;
338 }
339
340 /**
341 * Validate data for dynamic fields
342 * Set valid data in current object, also resets errors list
343 *
344 * @param array<string> $values Dynamic fields values
345 * @param string $prefix Prefix to replace, default to 'dynfield_'
346 *
347 * @return bool
348 */
349 public function dynamicsValidate(array $values, string $prefix = 'dynfield_'): bool
350 {
351 $dfields = [];
352 foreach ($values as $key => $value) {
353 $dfields[str_replace($prefix, $this->name_pattern, $key)] = $value;
354 }
355 return $this->dynamicsCheck($dfields, [], []);
356 }
357
358 /**
359 * Get form name
360 *
361 * @return string
362 */
363 public function getFormName(): string
364 {
365 return array_search(get_class($this), DynamicFieldsSet::getClasses());
366 }
367 }