1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace Leonidas\Framework\Site\Form; |
4
|
|
|
|
5
|
|
|
use Leonidas\Contracts\Auth\CsrfFieldPrinterInterface; |
6
|
|
|
use Leonidas\Contracts\Auth\CsrfManagerInterface; |
7
|
|
|
use Leonidas\Contracts\Extension\WpExtensionInterface; |
8
|
|
|
use Leonidas\Contracts\Http\Form\FormInterface; |
9
|
|
|
use Leonidas\Framework\Abstracts\AccessesSimpleCacheTrait; |
10
|
|
|
use Leonidas\Framework\Abstracts\FluentlySetsPropertiesTrait; |
11
|
|
|
use Leonidas\Framework\Abstracts\TranslatesTextTrait; |
12
|
|
|
use Leonidas\Framework\Abstracts\UtilizesExtensionTrait; |
13
|
|
|
use Leonidas\Library\Core\Auth\CsrfFieldPrinter; |
14
|
|
|
use Leonidas\Library\Core\Auth\Nonce; |
15
|
|
|
use Leonidas\Library\Core\Http\Policy\CsrfCheck; |
16
|
|
|
use Psr\Http\Message\ServerRequestInterface; |
17
|
|
|
use Psr\SimpleCache\CacheInterface; |
18
|
|
|
use WebTheory\HttpPolicy\ServerRequestPolicyInterface; |
19
|
|
|
use WebTheory\Saveyour\Auth\FormShield; |
20
|
|
|
use WebTheory\Saveyour\Contracts\Auth\FormShieldInterface; |
21
|
|
|
use WebTheory\Saveyour\Contracts\Controller\FormFieldControllerInterface; |
22
|
|
|
use WebTheory\Saveyour\Contracts\Controller\FormSubmissionManagerInterface; |
23
|
|
|
use WebTheory\Saveyour\Contracts\Data\FieldDataManagerInterface; |
24
|
|
|
use WebTheory\Saveyour\Contracts\Field\FormFieldInterface; |
25
|
|
|
use WebTheory\Saveyour\Contracts\Formatting\DataFormatterInterface; |
26
|
|
|
use WebTheory\Saveyour\Contracts\Processor\FormDataProcessorInterface; |
27
|
|
|
use WebTheory\Saveyour\Contracts\Report\ProcessedFormReportInterface; |
28
|
|
|
use WebTheory\Saveyour\Contracts\Report\ValidationReportInterface; |
29
|
|
|
use WebTheory\Saveyour\Contracts\Validation\ValidatorInterface; |
30
|
|
|
use WebTheory\Saveyour\Controller\Builder\FormFieldControllerBuilder; |
31
|
|
|
use WebTheory\Saveyour\Controller\FormSubmissionManager; |
32
|
|
|
use WebTheory\Saveyour\Data\SimpleCacheDataManager; |
33
|
|
|
use WebTheory\Saveyour\Field\Type\Hidden; |
34
|
|
|
use WebTheory\Saveyour\Processor\FailedValidationSimpleCache; |
35
|
|
|
use WebTheory\Saveyour\Processor\SimpleCacheSweeper; |
36
|
|
|
|
37
|
|
|
abstract class AbstractForm implements FormInterface |
38
|
|
|
{ |
39
|
|
|
use AccessesSimpleCacheTrait; |
40
|
|
|
use UtilizesExtensionTrait; |
41
|
|
|
use FluentlySetsPropertiesTrait; |
42
|
|
|
use TranslatesTextTrait; |
43
|
|
|
|
44
|
|
|
protected string $handle; |
45
|
|
|
|
46
|
|
|
protected string $action; |
47
|
|
|
|
48
|
|
|
/** |
49
|
|
|
* Fields being handled for the current request |
50
|
|
|
* |
51
|
|
|
* @var array<string> |
52
|
|
|
*/ |
53
|
|
|
protected array $processing; |
54
|
|
|
|
55
|
|
|
protected WpExtensionInterface $extension; |
56
|
|
|
|
57
|
|
|
public function __construct(WpExtensionInterface $extension) |
58
|
|
|
{ |
59
|
|
|
$this->extension = $extension; |
60
|
|
|
$this->init('construct'); |
61
|
|
|
} |
62
|
|
|
|
63
|
|
|
public function getHandle(): string |
64
|
|
|
{ |
65
|
|
|
return $this->handle; |
66
|
|
|
} |
67
|
|
|
|
68
|
|
|
public function getAction(): string |
69
|
|
|
{ |
70
|
|
|
return $this->action; |
71
|
|
|
} |
72
|
|
|
|
73
|
|
|
public function build(ServerRequestInterface $request): array |
74
|
|
|
{ |
75
|
|
|
$this->init('build'); |
76
|
|
|
$this->processing = $this->fields($request); |
77
|
|
|
|
78
|
|
|
$data = [ |
79
|
|
|
'method' => $this->formMethod(), |
80
|
|
|
'action' => $this->formAction($request), |
81
|
|
|
'checks' => $this->formChecks($request), |
82
|
|
|
'fields' => $this->formFields($request), |
83
|
|
|
'errors' => $this->formErrors($request), |
84
|
|
|
]; |
85
|
|
|
|
86
|
|
|
$this->resetStateProps(); |
87
|
|
|
|
88
|
|
|
return $data; |
89
|
|
|
} |
90
|
|
|
|
91
|
|
|
public function process(ServerRequestInterface $request): void |
92
|
|
|
{ |
93
|
|
|
$this->init('process'); |
94
|
|
|
$this->processing = $this->fields($request); |
95
|
|
|
|
96
|
|
|
$processed = $this->postProcess( |
97
|
|
|
$this->processor($request)->process($request), |
98
|
|
|
$request |
99
|
|
|
); |
100
|
|
|
|
101
|
|
|
$this->redirect($this->redirectTo($request, $processed)); |
|
|
|
|
102
|
|
|
} |
103
|
|
|
|
104
|
|
|
public function validate(ServerRequestInterface $request, string $field, $value): ValidationReportInterface |
105
|
|
|
{ |
106
|
|
|
$validators = $this->validation($request); |
107
|
|
|
|
108
|
|
|
return $validators[$field]->inspect($value); |
109
|
|
|
} |
110
|
|
|
|
111
|
|
|
protected function getExtension(): WpExtensionInterface |
112
|
|
|
{ |
113
|
|
|
return $this->extension; |
114
|
|
|
} |
115
|
|
|
|
116
|
|
|
protected function processor(ServerRequestInterface $request): FormSubmissionManagerInterface |
117
|
|
|
{ |
118
|
|
|
return new FormSubmissionManager( |
119
|
|
|
$this->fieldControllers($request), |
120
|
|
|
$this->processes($request), |
121
|
|
|
$this->shield($request), |
122
|
|
|
); |
123
|
|
|
} |
124
|
|
|
|
125
|
|
|
/** |
126
|
|
|
* @return array<FormFieldControllerInterface> |
127
|
|
|
*/ |
128
|
|
|
protected function fieldControllers(ServerRequestInterface $request): array |
129
|
|
|
{ |
130
|
|
|
$data = $this->dataManagers($request); |
131
|
|
|
$validation = $this->validation($request); |
132
|
|
|
$formatting = $this->formatting($request); |
133
|
|
|
|
134
|
|
|
$controllers = []; |
135
|
|
|
|
136
|
|
|
foreach ($this->processing as $field) { |
137
|
|
|
$name = $this->fieldKeyAsName($field); |
138
|
|
|
|
139
|
|
|
$controllers[] = FormFieldControllerBuilder::for($name) |
140
|
|
|
->dataManager($data[$field] ?? null) |
141
|
|
|
->formatter($formatting[$field] ?? null) |
142
|
|
|
->validator($validation[$field] ?? null) |
143
|
|
|
->get(); |
144
|
|
|
} |
145
|
|
|
|
146
|
|
|
return $controllers; |
147
|
|
|
} |
148
|
|
|
|
149
|
|
|
/** |
150
|
|
|
* @return array<string|array|FormFieldInterface> |
151
|
|
|
*/ |
152
|
|
|
protected function formFields(ServerRequestInterface $request): array |
153
|
|
|
{ |
154
|
|
|
$attributes = $this->attributes($request); |
155
|
|
|
$data = $this->dataManagers($request); |
156
|
|
|
$formatting = $this->formatting($request); |
157
|
|
|
|
158
|
|
|
$controllers = []; |
159
|
|
|
|
160
|
|
|
foreach ($this->processing as $field) { |
161
|
|
|
$name = $this->fieldKeyAsName($field); |
162
|
|
|
|
163
|
|
|
$controller = FormFieldControllerBuilder::for($name) |
164
|
|
|
->dataManager($data[$field] ?? null) |
165
|
|
|
->formatter($formatting[$field] ?? null) |
166
|
|
|
->get(); |
167
|
|
|
|
168
|
|
|
$definition = $attributes[$field] ?? []; |
169
|
|
|
|
170
|
|
|
$definition['name'] = $name; |
171
|
|
|
$definition['value'] = $controller->getPresetValue($request); |
172
|
|
|
|
173
|
|
|
$controllers[$field] = $definition; |
174
|
|
|
} |
175
|
|
|
|
176
|
|
|
return $controllers; |
177
|
|
|
} |
178
|
|
|
|
179
|
|
|
/** |
180
|
|
|
* @return array<string,array<int,string>> |
181
|
|
|
*/ |
182
|
|
|
protected function formErrors(ServerRequestInterface $request): array |
183
|
|
|
{ |
184
|
|
|
$errors = []; |
185
|
|
|
$messages = $this->errorMessages($request); |
186
|
|
|
$violations = $this->getCachedViolations($request); |
187
|
|
|
|
188
|
|
|
foreach ($violations as $field => $violations) { |
189
|
|
|
$field = $this->fieldNameAsKey($field); |
190
|
|
|
$errors[$field] = []; |
191
|
|
|
|
192
|
|
|
foreach ($violations as $violation) { |
193
|
|
|
$message = $messages[$field][$violation]; |
194
|
|
|
$errors[$field][$violation] = $this->translate($message); |
195
|
|
|
} |
196
|
|
|
} |
197
|
|
|
|
198
|
|
|
return $errors; |
199
|
|
|
} |
200
|
|
|
|
201
|
|
|
protected function getCachedViolations(ServerRequestInterface $request): array |
|
|
|
|
202
|
|
|
{ |
203
|
|
|
$cache = $this->simpleCache(); |
204
|
|
|
$key = $this->failedValidationCacheKey(); |
205
|
|
|
|
206
|
|
|
$violations = $cache->get($key, []); |
207
|
|
|
|
208
|
|
|
$cache->delete($key); |
209
|
|
|
|
210
|
|
|
return $violations; |
211
|
|
|
} |
212
|
|
|
|
213
|
|
|
protected function fieldKeyAsName(string $key): string |
214
|
|
|
{ |
215
|
|
|
return $this->prefix(str_replace('_', '-', $key), '-'); |
216
|
|
|
} |
217
|
|
|
|
218
|
|
|
protected function fieldKeysAsNames(array $keys): array |
219
|
|
|
{ |
220
|
|
|
return array_map([$this, 'fieldKeyAsName'], $keys); |
221
|
|
|
} |
222
|
|
|
|
223
|
|
|
protected function fieldNameAsKey(string $name): string |
224
|
|
|
{ |
225
|
|
|
$stripped = substr($name, strlen($this->extension->getPrefix()) + 1); |
226
|
|
|
|
227
|
|
|
return str_replace('-', '_', $stripped); |
228
|
|
|
} |
229
|
|
|
|
230
|
|
|
protected function fieldNamesAsKeys(array $names): array |
231
|
|
|
{ |
232
|
|
|
return array_map([$this, 'fieldNameAsKey'], $names); |
233
|
|
|
} |
234
|
|
|
|
235
|
|
|
protected function mappedResults(array $results): array |
236
|
|
|
{ |
237
|
|
|
$mapped = []; |
238
|
|
|
|
239
|
|
|
foreach ($results as $name => $value) { |
240
|
|
|
$mapped[$this->fieldNameAsKey($name)] = $value; |
241
|
|
|
} |
242
|
|
|
|
243
|
|
|
return $mapped; |
244
|
|
|
} |
245
|
|
|
|
246
|
|
|
protected function formMethod(): string |
247
|
|
|
{ |
248
|
|
|
return 'post'; |
249
|
|
|
} |
250
|
|
|
|
251
|
|
|
protected function formAction(ServerRequestInterface $request): string |
|
|
|
|
252
|
|
|
{ |
253
|
|
|
return esc_url(admin_url('admin-post.php')); |
|
|
|
|
254
|
|
|
} |
255
|
|
|
|
256
|
|
|
protected function formChecks(ServerRequestInterface $request): string |
257
|
|
|
{ |
258
|
|
|
return implode("\n", $this->checks($request)); |
259
|
|
|
} |
260
|
|
|
|
261
|
|
|
/** |
262
|
|
|
* @return array<string> |
263
|
|
|
*/ |
264
|
|
|
protected function checks(ServerRequestInterface $request): array |
|
|
|
|
265
|
|
|
{ |
266
|
|
|
return [ |
267
|
|
|
$this->actionFormCheck(), |
268
|
|
|
$this->refererFormCheck(), |
269
|
|
|
$this->csrfTokenFormCheck(), |
270
|
|
|
]; |
271
|
|
|
} |
272
|
|
|
|
273
|
|
|
protected function actionFormCheck(): string |
274
|
|
|
{ |
275
|
|
|
return (new Hidden()) |
276
|
|
|
->setName('action') |
277
|
|
|
->setValue($this->action) |
278
|
|
|
->toHtml(); |
279
|
|
|
} |
280
|
|
|
|
281
|
|
|
protected function refererFormCheck(): string |
282
|
|
|
{ |
283
|
|
|
return wp_referer_field(false); |
|
|
|
|
284
|
|
|
} |
285
|
|
|
|
286
|
|
|
protected function csrfTokenFormCheck(): string |
287
|
|
|
{ |
288
|
|
|
return $this->csrfPrinter()->print($this->token()); |
289
|
|
|
} |
290
|
|
|
|
291
|
|
|
protected function shield(ServerRequestInterface $request): FormShieldInterface |
292
|
|
|
{ |
293
|
|
|
return new FormShield($this->policies($request)); |
294
|
|
|
} |
295
|
|
|
|
296
|
|
|
/** |
297
|
|
|
* @return array<string,ServerRequestPolicyInterface> |
298
|
|
|
*/ |
299
|
|
|
protected function policies(ServerRequestInterface $request): array |
|
|
|
|
300
|
|
|
{ |
301
|
|
|
return [ |
302
|
|
|
'csrf' => $this->csrfAuthenticator(), |
303
|
|
|
]; |
304
|
|
|
} |
305
|
|
|
|
306
|
|
|
protected function csrfAuthenticator(): ServerRequestPolicyInterface |
307
|
|
|
{ |
308
|
|
|
return new CsrfCheck($this->token()); |
309
|
|
|
} |
310
|
|
|
|
311
|
|
|
protected function token(): CsrfManagerInterface |
312
|
|
|
{ |
313
|
|
|
return new Nonce($action = $this->getAction(), $action, Nonce::EXP_12); |
314
|
|
|
} |
315
|
|
|
|
316
|
|
|
/** |
317
|
|
|
* @return array<FormDataProcessorInterface> |
318
|
|
|
*/ |
319
|
|
|
protected function processes(ServerRequestInterface $request): array |
320
|
|
|
{ |
321
|
|
|
return [ |
322
|
|
|
$this->failedValidationCache(), |
323
|
|
|
$this->transientFieldCacheCleaner($request), |
324
|
|
|
]; |
325
|
|
|
} |
326
|
|
|
|
327
|
|
|
protected function failedValidationCache(): FormDataProcessorInterface |
328
|
|
|
{ |
329
|
|
|
return new FailedValidationSimpleCache( |
330
|
|
|
'cached_violations', |
331
|
|
|
$this->simpleCache(), |
332
|
|
|
$this->failedValidationCacheKey() |
333
|
|
|
); |
334
|
|
|
} |
335
|
|
|
|
336
|
|
|
protected function transientFieldCacheCleaner(ServerRequestInterface $request): FormDataProcessorInterface |
|
|
|
|
337
|
|
|
{ |
338
|
|
|
return new SimpleCacheSweeper( |
339
|
|
|
'transient_fields', |
340
|
|
|
$this->simpleCache(), |
341
|
|
|
array_map([$this, 'transientFieldCacheKey'], $this->processing) |
342
|
|
|
); |
343
|
|
|
} |
344
|
|
|
|
345
|
|
|
/** |
346
|
|
|
* @return array<string,null|FieldDataManagerInterface> |
347
|
|
|
*/ |
348
|
|
|
protected function dataManagers(ServerRequestInterface $request): array |
|
|
|
|
349
|
|
|
{ |
350
|
|
|
$managers = []; |
351
|
|
|
|
352
|
|
|
foreach ($this->processing as $key) { |
353
|
|
|
$managers[$key] = $this->transientDataManager($key); |
354
|
|
|
} |
355
|
|
|
|
356
|
|
|
return $managers; |
357
|
|
|
} |
358
|
|
|
|
359
|
|
|
protected function transientDataManager(string $key): FieldDataManagerInterface |
360
|
|
|
{ |
361
|
|
|
return new SimpleCacheDataManager( |
362
|
|
|
$this->getSimpleCache(), |
363
|
|
|
$this->transientFieldCacheKey($key) |
364
|
|
|
); |
365
|
|
|
} |
366
|
|
|
|
367
|
|
|
protected function redirect(?string $location = null): void |
368
|
|
|
{ |
369
|
|
|
wp_safe_redirect($location ?? $this->redirectDefault()); |
|
|
|
|
370
|
|
|
|
371
|
|
|
exit; |
|
|
|
|
372
|
|
|
} |
373
|
|
|
|
374
|
|
|
protected function redirectDefault(): string |
375
|
|
|
{ |
376
|
|
|
return wp_get_referer() ?: home_url(); |
|
|
|
|
377
|
|
|
} |
378
|
|
|
|
379
|
|
|
protected function simpleCache(): CacheInterface |
380
|
|
|
{ |
381
|
|
|
return $this->getService('transients_channel'); |
382
|
|
|
} |
383
|
|
|
|
384
|
|
|
protected function csrfPrinter(): CsrfFieldPrinterInterface |
385
|
|
|
{ |
386
|
|
|
return new CsrfFieldPrinter(); |
387
|
|
|
} |
388
|
|
|
|
389
|
|
|
protected function redirectTo(ServerRequestInterface $request, ProcessedFormReportInterface $report): ?string |
|
|
|
|
390
|
|
|
{ |
391
|
|
|
return null; |
392
|
|
|
} |
393
|
|
|
|
394
|
|
|
protected function postProcess(ProcessedFormReportInterface $report, ServerRequestInterface $request): ProcessedFormReportInterface |
|
|
|
|
395
|
|
|
{ |
396
|
|
|
return $report; |
397
|
|
|
} |
398
|
|
|
|
399
|
|
|
protected function resetStateProps(): void |
400
|
|
|
{ |
401
|
|
|
unset($this->processing); |
402
|
|
|
} |
403
|
|
|
|
404
|
|
|
protected function initiationContexts(): array |
405
|
|
|
{ |
406
|
|
|
return [ |
407
|
|
|
'construct' => $this->constructInitiationContext(), |
408
|
|
|
'build' => $this->buildInitiationContext(), |
409
|
|
|
'process' => $this->processInitiationContext(), |
410
|
|
|
]; |
411
|
|
|
} |
412
|
|
|
|
413
|
|
|
protected function cacheKey(string $sub): string |
414
|
|
|
{ |
415
|
|
|
return "form:{$this->handle}:{$sub}"; |
416
|
|
|
} |
417
|
|
|
|
418
|
|
|
protected function failedValidationCacheKey(): string |
419
|
|
|
{ |
420
|
|
|
return $this->cacheKey('errors'); |
421
|
|
|
} |
422
|
|
|
|
423
|
|
|
protected function transientFieldCacheKey(string $key): string |
424
|
|
|
{ |
425
|
|
|
return $this->cacheKey("submitted.{$key}"); |
426
|
|
|
} |
427
|
|
|
|
428
|
|
|
protected function constructInitiationContext(): array |
429
|
|
|
{ |
430
|
|
|
return ['handle', 'action']; |
431
|
|
|
} |
432
|
|
|
|
433
|
|
|
protected function buildInitiationContext(): array |
434
|
|
|
{ |
435
|
|
|
return ['simpleCache', 'localizer']; |
436
|
|
|
} |
437
|
|
|
|
438
|
|
|
protected function processInitiationContext(): array |
439
|
|
|
{ |
440
|
|
|
return ['simpleCache']; |
441
|
|
|
} |
442
|
|
|
|
443
|
|
|
/** |
444
|
|
|
* @return array<string,null|ValidatorInterface> |
445
|
|
|
*/ |
446
|
|
|
protected function validation(ServerRequestInterface $request): array |
|
|
|
|
447
|
|
|
{ |
448
|
|
|
return []; |
449
|
|
|
} |
450
|
|
|
|
451
|
|
|
/** |
452
|
|
|
* @return array<string,null|DataFormatterInterface> |
453
|
|
|
*/ |
454
|
|
|
protected function formatting(ServerRequestInterface $request): array |
|
|
|
|
455
|
|
|
{ |
456
|
|
|
return []; |
457
|
|
|
} |
458
|
|
|
|
459
|
|
|
protected function action(): string |
460
|
|
|
{ |
461
|
|
|
return $this->prefix($this->getHandle(), '_'); |
462
|
|
|
} |
463
|
|
|
|
464
|
|
|
abstract protected function handle(): string; |
465
|
|
|
|
466
|
|
|
/** |
467
|
|
|
* @return array<string> |
468
|
|
|
*/ |
469
|
|
|
abstract protected function fields(ServerRequestInterface $request): array; |
470
|
|
|
|
471
|
|
|
/** |
472
|
|
|
* @return array<string,array<string,mixed>|FormFieldInterface> |
473
|
|
|
*/ |
474
|
|
|
abstract protected function attributes(ServerRequestInterface $request): array; |
475
|
|
|
|
476
|
|
|
/** |
477
|
|
|
* @return array<string,array<string,string>> |
478
|
|
|
*/ |
479
|
|
|
abstract protected function errorMessages(ServerRequestInterface $request): array; |
480
|
|
|
} |
481
|
|
|
|
This check looks for function or method calls that always return null and whose return value is used.
The method
getObject()
can return nothing but null, so it makes no sense to use the return value.The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.