Lines 85.78% 356 / 415
Methods 36.36% 8 / 22
Classes 0.00% 0 / 1
Name Lines Methods CRAP
 readResponse 100.00% 68 / 68 100.00% 1 / 1 3
 readAuthorizedWorkstation 100.00% 11 / 11 100.00% 1 / 1 2
 readQueryParameters 100.00% 16 / 16 100.00% 1 / 1 1
 readScopeList 100.00% 6 / 6 100.00% 1 / 1 1
 withScopesThatHaveProvider 100.00% 6 / 6 100.00% 1 / 1 3
 readProviderId 83.33% 5 / 6 0.00% 0 / 1 4.07
 resolveSourceScopeId 53.33% 8 / 15 0.00% 0 / 1 9.66
 resolveSourceScope 37.50% 6 / 16 0.00% 0 / 1 7.91
 readCustomTemplates 90.90% 20 / 22 0.00% 0 / 1 4.01
 processPostRequest 83.33% 45 / 54 0.00% 0 / 1 10.46
 validateTargetScopes 74.07% 20 / 27 0.00% 0 / 1 5.44
 copyTemplate 83.33% 30 / 36 0.00% 0 / 1 4.07
 readPositiveInt 81.81% 9 / 11 0.00% 0 / 1 6.22
 readTargetScopeIds 81.81% 9 / 11 0.00% 0 / 1 4.10
 countUniqueProviders 90.90% 10 / 11 0.00% 0 / 1 4.01
 buildTargetScopeOptions 97.43% 38 / 39 0.00% 0 / 1 7
 isEligibleCopyTarget 90.00% 9 / 10 0.00% 0 / 1 4.02
 formatScopeName 44.44% 4 / 9 0.00% 0 / 1 2.69
 [BO\Zmsadmin\BaseController] __invoke 100.00% 3 / 3 100.00% 1 / 1 1
 [BO\Zmsadmin\BaseController] getSchemaConstraintList 100.00% 8 / 8 100.00% 1 / 1 4
 [BO\Zmsadmin\BaseController] transformValidationErrors 61.53% 8 / 13 0.00% 0 / 1 15.69
 [BO\Zmsadmin\BaseController] handleEntityWrite 100.00% 17 / 17 100.00% 1 / 1 5
16class MailTemplatesCopy extends BaseController
17{
18    /**
19     * @return ResponseInterface
20     */
21    #[\Override]
22    public function readResponse(
23        RequestInterface $request,
24        ResponseInterface $response,
25        array $args
26    ): ResponseInterface {
27        $workstation = $this->readAuthorizedWorkstation();
28        $queryParameters = $this->readQueryParameters($request);
29        $scopeList = $this->readScopeList($workstation);
30
31        $input = (array) ($request->getParsedBody() ?? []);
32        $isPostRequest = strtoupper($request->getMethod()) === 'POST';
33
34        $sourceScopeId = $this->resolveSourceScopeId(
35            $input,
36            $queryParameters['sourceScopeId'],
37            $isPostRequest,
38            $workstation,
39            $scopeList
40        );
41
42        [
43            $selectedSourceScope,
44            $formError,
45        ] = $this->resolveSourceScope(
46            $sourceScopeId,
47            $isPostRequest,
48            $scopeList
49        );
50
51        $customTemplates = $this->readCustomTemplates(
52            $selectedSourceScope
53        );
54
55        $sourceTemplateId = null;
56        $selectedTargetScopeIds = [];
57
58        if ($isPostRequest) {
59            $postResult = $this->processPostRequest(
60                $input,
61                $sourceScopeId,
62                $selectedSourceScope,
63                $scopeList,
64                $customTemplates,
65                $formError
66            );
67
68            if ($postResult['response'] instanceof ResponseInterface) {
69                return $postResult['response'];
70            }
71
72            $sourceTemplateId =
73                $postResult['sourceTemplateId'];
74
75            $selectedTargetScopeIds =
76                $postResult['selectedTargetScopeIds'];
77
78            $formError = $postResult['formError'];
79        }
80
81        return Render::withHtml(
82            $response,
83            'page/mailtemplatesCopy.twig',
84            [
85                'title' => 'E-Mail-Template kopieren',
86                'pageTitle' => 'E-Mail-Template kopieren',
87                'menuActive' => 'mailtemplatesCopy',
88                'workstation' => $workstation,
89                'scopeList' => $scopeList,
90                'sourceScopeId' => $sourceScopeId,
91                'selectedSourceScope' => $selectedSourceScope,
92                'customTemplates' => $customTemplates,
93                'selectedSourceTemplateId' => $sourceTemplateId,
94                'selectedTargetScopeIds' =>
95                    $selectedTargetScopeIds,
96                'formError' => $formError,
97                'success' => $queryParameters['success'],
98                'copiedCount' =>
99                    $queryParameters['copiedCount'],
100                'targetScopeOptions' =>
101                    $this->buildTargetScopeOptions(
102                        $workstation,
103                        $scopeList,
104                        $selectedSourceScope
105                    ),
106            ]
107        );
108    }
109
110    private function readAuthorizedWorkstation(): Workstation
111    {
112        $workstation = \App::$http
113            ->readGetResult(
114                '/workstation/',
115                ['resolveReferences' => 3]
116            )
117            ->getEntity();
118
119        if (
120            !$workstation
121                ->getUseraccount()
122                ->hasPermissions(['mailtemplates'])
123        ) {
124            throw new UserAccountMissingRights();
125        }
126
127        return $workstation;
128    }
129
130    private function readQueryParameters(
131        RequestInterface $request
132    ): array {
133        $validator = $request->getAttribute('validator');
134
135        return [
136            'success' => $validator
137                ->getParameter('success')
138                ->isString()
139                ->getValue(),
140            'copiedCount' => (int) $validator
141                ->getParameter('copiedCount')
142                ->isNumber()
143                ->setDefault(0)
144                ->getValue(),
145            'sourceScopeId' => $validator
146                ->getParameter('sourceScopeId')
147                ->isNumber()
148                ->getValue(),
149        ];
150    }
151
152    private function readScopeList(Workstation $workstation): ScopeList
153    {
154        /*
155         * Dieselbe Menge wie backend EntityAccess/hasScope():
156         * zugewiesene Departments inklusive Cluster-Standorte.
157         */
158        $scopeList = $workstation
159            ->getUseraccount()
160            ->getDepartmentList()
161            ->getUniqueScopeList()
162            ->sortByContactName();
163
164        return $this->withScopesThatHaveProvider($scopeList);
165    }
166
167    private function withScopesThatHaveProvider(
168        ScopeList $scopeList
169    ): ScopeList {
170        $authorizedScopeList = new ScopeList();
171
172        foreach ($scopeList as $scope) {
173            if ($this->readProviderId($scope) === '') {
174                continue;
175            }
176
177            $authorizedScopeList->addEntity($scope);
178        }
179
180        return $authorizedScopeList;
181    }
182
183    private function readProviderId(Scope $scope): string
184    {
185        try {
186            $providerId = (string) $scope->getProviderId();
187        } catch (ScopeMissingProvider) {
188            return '';
189        }
190
191        if ($providerId === '' || $providerId === '0') {
192            return '';
193        }
194
195        return $providerId;
196    }
197
198    private function resolveSourceScopeId(
199        array $input,
200        mixed $requestedSourceScopeId,
201        bool $isPostRequest,
202        Workstation $workstation,
203        ScopeList $scopeList
204    ): ?int {
205        $sourceScopeValue = $isPostRequest
206            ? ($input['sourceScopeId'] ?? null)
207            : $requestedSourceScopeId;
208
209        $sourceScopeId = $this->readPositiveInt(
210            $sourceScopeValue
211        );
212
213        if ($sourceScopeId !== null || $isPostRequest) {
214            return $sourceScopeId;
215        }
216
217        /*
218         * Beim ersten Seitenaufruf wird der aktuell ausgewählte
219         * Arbeitsplatzstandort als Quelle vorausgewählt.
220         */
221        $currentScopeId = $this->readPositiveInt(
222            $workstation->getScope()->getId()
223        );
224
225        if (
226            $currentScopeId !== null
227            && $scopeList->hasEntity($currentScopeId)
228        ) {
229            return $currentScopeId;
230        }
231
232        return null;
233    }
234
235    private function resolveSourceScope(
236        ?int $sourceScopeId,
237        bool $isPostRequest,
238        ScopeList $scopeList
239    ): array {
240        if ($sourceScopeId === null) {
241            $formError = $isPostRequest
242                ? 'Bitte wählen Sie einen gültigen '
243                    . 'Quellstandort aus.'
244                : null;
245
246            return [null, $formError];
247        }
248
249        $selectedSourceScope = $scopeList->getEntity(
250            $sourceScopeId
251        );
252
253        if ($selectedSourceScope === null) {
254            return [
255                null,
256                'Der ausgewählte Quellstandort wurde nicht '
257                    . 'gefunden oder darf nicht verwendet werden.',
258            ];
259        }
260
261        return [$selectedSourceScope, null];
262    }
263
264    private function readCustomTemplates(
265        ?Scope $selectedSourceScope
266    ): MailtemplateList {
267        $customTemplates = new MailtemplateList();
268
269        if ($selectedSourceScope === null) {
270            return $customTemplates;
271        }
272
273        $providerId = $this->readProviderId(
274            $selectedSourceScope
275        );
276
277        if ($providerId === '') {
278            return $customTemplates;
279        }
280
281        $loadedTemplates = \App::$http
282            ->readGetResult(
283                '/custom-mailtemplates/' . $providerId . '/'
284            )
285            ->getCollection();
286
287        if ($loadedTemplates === null) {
288            return $customTemplates;
289        }
290
291        $loadedTemplates->prioritizeByName([
292            'mail_preconfirmed.twig',
293            'mail_confirmation.twig',
294            'mail_reminder.twig',
295            'mail_delete.twig',
296        ]);
297
298        return $loadedTemplates;
299    }
300
301    private function processPostRequest(
302        array $input,
303        ?int $sourceScopeId,
304        ?Scope $selectedSourceScope,
305        ScopeList $scopeList,
306        MailtemplateList $customTemplates,
307        ?string $formError
308    ): array {
309        $sourceTemplateId = $this->readPositiveInt(
310            $input['sourceTemplateId'] ?? null
311        );
312
313        $normalizedTargetScopeIds =
314            $this->readTargetScopeIds(
315                $input['targetScopeIds'] ?? null
316            );
317
318        $result = [
319            'sourceTemplateId' => $sourceTemplateId,
320            'selectedTargetScopeIds' =>
321                $normalizedTargetScopeIds ?? [],
322            'formError' => $formError,
323            'response' => null,
324        ];
325
326        if ($result['formError'] !== null) {
327            return $result;
328        }
329
330        if (
331            $sourceScopeId === null
332            || $selectedSourceScope === null
333        ) {
334            $result['formError'] =
335                'Bitte wählen Sie einen gültigen '
336                . 'Quellstandort aus.';
337
338            return $result;
339        }
340
341        if (
342            $sourceTemplateId === null
343            || !$customTemplates->hasEntity(
344                $sourceTemplateId
345            )
346        ) {
347            $result['formError'] =
348                'Bitte wählen Sie ein angepasstes '
349                . 'E-Mail-Template des Quellstandorts aus.';
350
351            return $result;
352        }
353
354        if (
355            $normalizedTargetScopeIds === null
356            || $normalizedTargetScopeIds === []
357        ) {
358            $result['formError'] =
359                'Bitte wählen Sie mindestens einen '
360                . 'Zielstandort aus.';
361
362            return $result;
363        }
364
365        $targetScopeError = $this->validateTargetScopes(
366            $normalizedTargetScopeIds,
367            $selectedSourceScope,
368            $scopeList
369        );
370
371        if ($targetScopeError !== null) {
372            $result['formError'] = $targetScopeError;
373
374            return $result;
375        }
376
377        $copyResult = $this->copyTemplate(
378            $sourceScopeId,
379            $sourceTemplateId,
380            $normalizedTargetScopeIds,
381            $scopeList
382        );
383
384        if ($copyResult instanceof ResponseInterface) {
385            $result['response'] = $copyResult;
386        } else {
387            $result['formError'] = $copyResult;
388        }
389
390        return $result;
391    }
392
393    private function validateTargetScopes(
394        array $targetScopeIds,
395        Scope $selectedSourceScope,
396        ScopeList $scopeList
397    ): ?string {
398        foreach ($targetScopeIds as $targetScopeId) {
399            $targetScope = $scopeList->getEntity(
400                $targetScopeId
401            );
402
403            if ($targetScope === null) {
404                return
405                    'Mindestens ein ausgewählter Zielstandort '
406                    . 'wurde nicht gefunden oder darf nicht '
407                    . 'verwendet werden.';
408            }
409
410            $isSourceScope =
411                $targetScopeId
412                === $selectedSourceScope->getId();
413
414            if ($isSourceScope) {
415                return
416                    'Der Quellstandort darf nicht gleichzeitig '
417                    . 'als Zielstandort ausgewählt werden.';
418            }
419
420            $hasSameProvider =
421                $this->readProviderId($targetScope)
422                ===
423                $this->readProviderId($selectedSourceScope);
424
425            if ($hasSameProvider) {
426                return
427                    'Der ausgewählte Zielstandort verwendet '
428                    . 'denselben Provider wie der Quellstandort '
429                    . 'und kann daher nicht als Zielstandort '
430                    . 'verwendet werden.';
431            }
432        }
433
434        return null;
435    }
436
437    private function copyTemplate(
438        int $sourceScopeId,
439        int $sourceTemplateId,
440        array $targetScopeIds,
441        ScopeList $scopeList
442    ): ResponseInterface|string {
443        try {
444            $copiedTemplate = \App::$http
445                ->readPostResult(
446                    '/mailtemplates/copy/',
447                    [
448                        'sourceScopeId' => $sourceScopeId,
449                        'sourceTemplateId' =>
450                            $sourceTemplateId,
451                        'targetScopeIds' => $targetScopeIds,
452                    ]
453                )
454                ->getEntity();
455        } catch (Exception $exception) {
456            $backendMessage = trim(
457                (string) $exception->originalMessage
458            );
459
460            if ($backendMessage !== '') {
461                return $backendMessage;
462            }
463
464            return
465                'Beim Kopieren des E-Mail-Templates ist ein '
466                . 'Fehler aufgetreten.';
467        }
468
469        if ($copiedTemplate === null) {
470            return
471                'Das E-Mail-Template konnte nicht kopiert '
472                . 'werden.';
473        }
474
475        return Render::redirect(
476            'mailtemplatesCopy',
477            [],
478            [
479                'sourceScopeId' => $sourceScopeId,
480                'success' => 'mailtemplates_copied',
481                'copiedCount' => $this->countUniqueProviders(
482                    $targetScopeIds,
483                    $scopeList
484                ),
485            ]
486        );
487    }
488
489    private function readPositiveInt(mixed $value): ?int
490    {
491        if (
492            (!is_int($value) && !is_string($value))
493            || is_bool($value)
494        ) {
495            return null;
496        }
497
498        $validatedValue = filter_var(
499            $value,
500            FILTER_VALIDATE_INT
501        );
502
503        if (
504            $validatedValue === false
505            || $validatedValue < 1
506        ) {
507            return null;
508        }
509
510        return (int) $validatedValue;
511    }
512
513    private function readTargetScopeIds(
514        mixed $value
515    ): ?array {
516        if (!is_array($value)) {
517            return null;
518        }
519
520        $scopeIds = [];
521
522        foreach ($value as $scopeId) {
523            $validatedScopeId = $this->readPositiveInt(
524                $scopeId
525            );
526
527            if ($validatedScopeId === null) {
528                return null;
529            }
530
531            $scopeIds[] = $validatedScopeId;
532        }
533
534        return array_values(array_unique($scopeIds));
535    }
536
537    private function countUniqueProviders(
538        array $targetScopeIds,
539        ScopeList $scopeList
540    ): int {
541        $providerIds = [];
542
543        foreach ($targetScopeIds as $targetScopeId) {
544            $targetScope = $scopeList->getEntity(
545                $targetScopeId
546            );
547
548            if ($targetScope === null) {
549                continue;
550            }
551
552            $providerId = $this->readProviderId($targetScope);
553
554            if ($providerId !== '') {
555                $providerIds[] = $providerId;
556            }
557        }
558
559        return count(array_unique($providerIds));
560    }
561
562    /**
563     * @return array<int, array{name: string, options: array<int, array{value: int, name: string}>}>
564     */
565    private function buildTargetScopeOptions(
566        Workstation $workstation,
567        ScopeList $scopeList,
568        ?Scope $selectedSourceScope
569    ): array {
570        if ($selectedSourceScope === null) {
571            return [];
572        }
573
574        $departments = $workstation
575            ->getUseraccount()
576            ->getDepartmentList()
577            ->withMatchingScopes($scopeList);
578
579        $seenProviderIds = [];
580        $targetScopeOptions = [];
581
582        foreach ($departments as $department) {
583            $departmentScopeOptions = [];
584
585            foreach ($department->scopes as $scope) {
586                $providerId = $this->readProviderId($scope);
587
588                if (
589                    !$this->isEligibleCopyTarget(
590                        $scope,
591                        $selectedSourceScope
592                    )
593                    || isset($seenProviderIds[$providerId])
594                ) {
595                    continue;
596                }
597
598                $seenProviderIds[$providerId] = true;
599                $departmentScopeOptions[] = [
600                    'value' => (int) $scope->getId(),
601                    'name' => $this->formatScopeName($scope),
602                ];
603            }
604
605            if ($departmentScopeOptions === []) {
606                continue;
607            }
608
609            usort(
610                $departmentScopeOptions,
611                static function (array $left, array $right): int {
612                    return strnatcasecmp(
613                        (string) $left['name'],
614                        (string) $right['name']
615                    );
616                }
617            );
618
619            $targetScopeOptions[] = [
620                'name' => (string) $department->name,
621                'options' => $departmentScopeOptions,
622            ];
623        }
624
625        return $targetScopeOptions;
626    }
627
628    private function isEligibleCopyTarget(
629        Scope $scope,
630        Scope $selectedSourceScope
631    ): bool {
632        if (
633            (int) $scope->getId()
634            === (int) $selectedSourceScope->getId()
635        ) {
636            return false;
637        }
638
639        $scopeProviderId = $this->readProviderId($scope);
640        $sourceProviderId = $this->readProviderId(
641            $selectedSourceScope
642        );
643
644        if ($scopeProviderId === '' || $sourceProviderId === '') {
645            return false;
646        }
647
648        return $scopeProviderId !== $sourceProviderId;
649    }
650
651    private function formatScopeName(Scope $scope): string
652    {
653        $contactName = trim((string) $scope->getName());
654        $shortName = trim((string) $scope->getShortName());
655
656        if ($contactName !== '') {
657            return trim($contactName . ' ' . $shortName);
658        }
659
660        return sprintf(
661            '(Standort gelöscht, ehemals: %s-%s)',
662            (string) ($scope->provider['source'] ?? ''),
663            $this->readProviderId($scope)
664        );
665    }
666}

Inherited from BO\Zmsadmin\BaseController

21    public function __invoke(RequestInterface $request, ResponseInterface $response, array $args)
22    {
23        $request = $this->initRequest($request);
24        $noCacheResponse = \BO\Slim\Render::withLastModified($response, time(), '0');
25        return $this->readResponse($request, $noCacheResponse, $args);
26    }
41    public function getSchemaConstraintList($schema): array
42    {
43        $list = [];
44        $locale = \App::$language->getLocale();
45        foreach ($schema->properties as $key => $property) {
46            if (isset($property['x-locale'])) {
47                $constraints = $property['x-locale'][$locale];
48                if ($constraints) {
49                    $list[$key]['description'] = $constraints['messages'];
50                }
51            }
52        }
53        return $list;
54    }
65    protected function transformValidationErrors($errorData)
66    {
67        if (!is_array($errorData) && !($errorData instanceof \Traversable)) {
68            return [];
69        }
70        $transformed = [];
71        foreach ($errorData as $pointer => $item) {
72            // Extract field name from JSON pointer (e.g., "/id" -> "id", "/contact/email" -> "contact/email")
73            // If the key doesn't start with "/", it's already a field name, so use it as-is
74            $fieldName = (strpos($pointer, '/') === 0) ? ltrim($pointer, '/') : $pointer;
75            // Handle root level errors
76            if ($fieldName === '' || $fieldName === null) {
77                $fieldName = '_root';
78            }
79            // Ensure the item structure is correct (has 'messages' array)
80            if (is_array($item) && isset($item['messages'])) {
81                $transformed[$fieldName] = $item;
82            } elseif (is_array($item)) {
83                // If item is an array but doesn't have 'messages', wrap it
84                $transformed[$fieldName] = $item;
85            } else {
86                $transformed[$fieldName] = $item;
87            }
88        }
89        return $transformed;
90    }
99    protected function handleEntityWrite(callable $httpCall)
100    {
101        try {
102            return $httpCall();
103        } catch (\BO\Zmsclient\Exception $exception) {
104            if ('BO\Zmsentities\Exception\SchemaValidation' == $exception->template) {
105                return [
106                    'template' => 'exception/bo/zmsentities/exception/schemavalidation.twig',
107                    'include' => true,
108                    'data' => $this->transformValidationErrors($exception->data)
109                ];
110            }
111
112            $template = TwigExceptionHandler::getExceptionTemplate($exception);
113            if (
114                '' != $exception->template
115                && \App::$slim->getContainer()->get('view')->getLoader()->exists($template)
116            ) {
117                return [
118                    'template' => $template,
119                    'include' => true,
120                    'data' => $this->transformValidationErrors($exception->data)
121                ];
122            }
123
124            throw $exception;
125        }
126    }