PaymentOrderController   A
last analyzed

Complexity

Total Complexity 34

Size/Duplication

Total Lines 241
Duplicated Lines 0 %

Importance

Changes 5
Bugs 0 Features 0
Metric Value
eloc 135
c 5
b 0
f 0
dl 0
loc 241
rs 9.68
wmc 34

4 Methods

Rating   Name   Duplication   Size   Complexity  
A copyProperties() 0 7 1
B new() 0 103 10
A __construct() 0 4 1
F confirmation() 0 110 22
1
<?php
2
/*
3
 * Copyright (C) 2020  Jan Böhmer
4
 *
5
 * This program is free software: you can redistribute it and/or modify
6
 * it under the terms of the GNU Affero General Public License as published
7
 * by the Free Software Foundation, either version 3 of the License, or
8
 * (at your option) any later version.
9
 *
10
 * This program is distributed in the hope that it will be useful,
11
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13
 * GNU Affero General Public License for more details.
14
 *
15
 * You should have received a copy of the GNU Affero General Public License
16
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
17
 */
18
19
namespace App\Controller;
20
21
use App\Audit\UserProvider;
22
use App\Entity\PaymentOrder;
23
use App\Event\PaymentOrderSubmittedEvent;
24
use App\Form\PaymentOrderConfirmationType;
25
use App\Form\PaymentOrderType;
26
use App\Message\PaymentOrder\PaymentOrderDeletedNotification;
27
use App\Services\PaymentReferenceGenerator;
28
use DateTime;
29
use Doctrine\ORM\EntityManagerInterface;
30
use InvalidArgumentException;
31
use RuntimeException;
32
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
33
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
34
use Symfony\Component\Form\Form;
35
use Symfony\Component\Form\SubmitButton;
36
use Symfony\Component\HttpFoundation\Request;
37
use Symfony\Component\HttpFoundation\Response;
38
use Symfony\Component\RateLimiter\RateLimiterFactory;
39
use Symfony\Component\Routing\Annotation\Route;
40
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
41
42
/**
43
 * This controller handles the payment order submit form.
44
 *
45
 * @Route("/payment_order")
46
 */
47
class PaymentOrderController extends AbstractController
48
{
49
    private $userProvider;
50
    private $entityManager;
51
52
    public function __construct(UserProvider $userProvider, EntityManagerInterface $entityManager)
53
    {
54
        $this->userProvider = $userProvider;
55
        $this->entityManager = $entityManager;
56
    }
57
58
    /**
59
     * @Route("/new", name="payment_order_new")
60
     */
61
    public function new(Request $request, EntityManagerInterface $entityManager, EventDispatcherInterface $dispatcher,
62
        PaymentReferenceGenerator $paymentReferenceGenerator, RateLimiterFactory $paymentOrderSubmitLimiter): Response
63
    {
64
        $limiter = $paymentOrderSubmitLimiter->create($request->getClientIp());
65
66
        $new_order = new PaymentOrder();
67
68
        $blocked_token = $request->get('blocked_token');
69
70
        //Skip fsr blocked validation if a token was given (but it is not validated yet if the token is correct)
71
        $validation_groups = ['Default', 'frontend'];
72
        if (!$blocked_token) {
73
            $validation_groups[] = 'fsr_blocked';
74
        }
75
76
        $form = $this->createForm(PaymentOrderType::class, $new_order, [
77
            'validation_groups' => $validation_groups,
78
        ]);
79
80
        if (!$form instanceof Form) {
81
            throw new InvalidArgumentException('$form must be a Form object!');
82
        }
83
84
        $form->handleRequest($request);
85
86
        if ($form->isSubmitted()) {
87
            if ($form->isValid()) {
88
                /* Limit the amount of how many payment orders can be submitted by one user in an hour
89
                    This prevents automatic mass creation of payment orders and also prevents that skip token can
90
                    guessed by brute force */
91
                $limiter->consume(1)
92
                    ->ensureAccepted();
93
94
                //We know now the department and can check if token was valid
95
                //If it isn't, then show an flash and dont save the payment order
96
                if ($blocked_token && !$new_order->getDepartment()->isSkipBlockedValidationToken($blocked_token)) {
97
                    $this->addFlash('error', 'payment_order.flash.invalid_blocked_token');
98
                } else {
99
                    $entityManager->persist($new_order);
100
101
                    //Invalidate blocked token if one was given
102
                    if ($blocked_token) {
103
                        $new_order->getDepartment()
104
                            ->invalidateSkipBlockedValidationToken($blocked_token);
105
                    }
106
107
                    $username = sprintf('%s %s (%s) [New PaymentOrder]',
108
                        $new_order->getFirstName(),
109
                        $new_order->getLastName(),
110
                        $new_order->getContactEmail()
111
                    );
112
                    $this->userProvider->setManualUsername($username, $new_order->getContactEmail());
113
114
                    $entityManager->flush();
115
116
                    //We have to do this after the first flush, as we need to know the ID
117
                    $this->userProvider->setManualUsername('[Automatic payment reference generation]',
118
                        UserProvider::INTERNAL_USER_IDENTIFIER);
119
                    $paymentReferenceGenerator->setPaymentReference($new_order);
120
                    $entityManager->flush();
121
122
                    $this->addFlash('success', 'flash.saved_successfully');
123
124
                    //Dispatch event so an email can be sent
125
                    $event = new PaymentOrderSubmittedEvent($new_order);
126
                    $dispatcher->dispatch($event, $event::NAME);
127
128
                    //Redirect to homepage, if no further paymentOrders should be submitted
129
                    //Otherwise create a new form for further ones
130
                    if ('submit' === $form->getClickedButton()->getName()) {
131
                        return $this->redirectToRoute('homepage');
132
                    }
133
134
                    if ('submit_new' === $form->getClickedButton()->getName()) {
135
                        $old_order = $new_order;
136
                        $new_order = new PaymentOrder();
137
                        $this->copyProperties($old_order, $new_order);
138
139
                        $form = $this->createForm(PaymentOrderType::class, $new_order);
140
                    }
141
                }
142
            } else {
143
                $this->addFlash('error', 'flash.error.check_input');
144
            }
145
        }
146
147
        $limit = $limiter->consume(0);
148
149
        $response = $this->render('PaymentOrder/form.html.twig', [
150
            'form' => $form->createView(),
151
            'entity' => $new_order,
152
        ]);
153
154
        $response->headers->add(
155
            [
156
                'X-RateLimit-Remaining' => $limit->getRemainingTokens(),
157
                'X-RateLimit-Retry-After' => $limit->getRetryAfter()
158
                    ->getTimestamp(),
159
                'X-RateLimit-Limit' => $limit->getLimit(),
160
            ]
161
        );
162
163
        return $response;
164
    }
165
166
    private function copyProperties(PaymentOrder $source, PaymentOrder $target): void
167
    {
168
        $target->setFirstName($source->getFirstName());
169
        $target->setLastName($source->getLastName());
170
        $target->setContactEmail($source->getContactEmail());
171
        $target->setDepartment($source->getDepartment());
172
        $target->setBankInfo($source->getBankInfo());
173
    }
174
175
    /**
176
     * @Route("/{id}/confirm", name="payment_order_confirm")
177
     */
178
    public function confirmation(?PaymentOrder $paymentOrder, Request $request, EntityManagerInterface $em): Response
179
    {
180
        if($paymentOrder === null) {
181
            $this->addFlash('error', 'payment_order.can_not_be_found');
182
            return $this->redirectToRoute('homepage');
183
        }
184
185
        //Check if we have one of the valid confirm numbers
186
        $confirm_step = $request->query->getInt('confirm');
187
        if (1 !== $confirm_step && 2 !== $confirm_step) {
188
            $this->addFlash('error', 'payment_order.confirmation.invalid_step');
189
190
            return $this->redirectToRoute('homepage');
191
        }
192
193
        //Check if given token is correct for this step
194
        $correct_token = (1 === $confirm_step) ? $paymentOrder->getConfirm1Token() : $paymentOrder->getConfirm2Token();
195
        if (null === $correct_token) {
196
            throw new RuntimeException('This payment_order can not be confirmed! No token is set.');
197
        }
198
199
        $given_token = (string) $request->query->get('token');
200
        if (!password_verify($given_token, $correct_token)) {
201
            $this->addFlash('error', 'payment_order.confirmation.invalid_token');
202
203
            return $this->redirectToRoute('homepage');
204
        }
205
206
        $already_confirmed = false;
207
208
        //Check if it was already confirmed from this side and disable form if needed
209
        $confirm_timestamp = (1 === $confirm_step) ? $paymentOrder->getConfirm1Timestamp() : $paymentOrder->getConfirm2Timestamp();
210
        if (null !== $confirm_timestamp) {
211
            $already_confirmed = true;
212
        }
213
        $form = $this->createForm(PaymentOrderConfirmationType::class, null, [
214
            'disabled' => null !== $confirm_timestamp,
215
        ]);
216
217
        $paymentOrder_is_undeletable = $paymentOrder->isExported()
218
            || $paymentOrder->isMathematicallyCorrect()
219
            || $paymentOrder->isFactuallyCorrect()
220
            || null != $paymentOrder->getBookingDate();
221
222
        $deletion_form = $this->createFormBuilder()
223
            ->add('delete', SubmitType::class, [
224
                'disabled' => $paymentOrder_is_undeletable,
225
                'label' => 'payment_order.confirm.delete.btn',
226
                'attr' => [
227
                    'class' => 'btn btn-danger'
228
                ]
229
            ])->getForm();
230
231
        //Handle deletion form
232
        $deletion_form->handleRequest($request);
233
        if ($deletion_form->isSubmitted() && $deletion_form->isValid()) {
234
            if ($paymentOrder_is_undeletable) {
235
                throw new RuntimeException("This payment order is already exported or booked and therefore can not be deleted by user!");
236
            }
237
238
            $blame_user = "unknown";
239
            if ($confirm_step === 1) {
240
                $blame_user = implode(",", $paymentOrder->getDepartment()->getEmailHhv());
241
            } elseif ($confirm_step === 2) {
242
                $blame_user = implode(',', $paymentOrder->getDepartment()->getEmailTreasurer());
243
            }
244
245
            $message = new PaymentOrderDeletedNotification($paymentOrder, $blame_user, PaymentOrderDeletedNotification::DELETED_WHERE_FRONTEND);
246
            $this->dispatchMessage($message);
0 ignored issues
show
Deprecated Code introduced by
The function Symfony\Bundle\Framework...ller::dispatchMessage() has been deprecated: since Symfony 5.4, inject an instance of MessageBusInterface in your controller instead ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

246
            /** @scrutinizer ignore-deprecated */ $this->dispatchMessage($message);

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
247
248
            $this->entityManager->remove($paymentOrder);
249
            $this->entityManager->flush();
250
251
            $this->addFlash('success', 'payment_order.confirmation.delete.success');
252
            return $this->redirectToRoute('homepage');
253
        }
254
255
        //Handle confirmation form
256
        $form->handleRequest($request);
257
        if ($form->isSubmitted() && $form->isValid()) {
258
            $this->addFlash('success', 'payment_order.confirmation.success');
259
            //Write confirmation to DB
260
            if (1 === $confirm_step) {
261
                $paymentOrder->setConfirm1Timestamp(new DateTime());
262
            } elseif (2 === $confirm_step) {
263
                $paymentOrder->setConfirm2Timestamp(new DateTime());
264
            }
265
266
            //Add hintful information about who did this, to audit log
267
            $emails = (1 === $confirm_step) ? $paymentOrder->getDepartment()
268
                ->getEmailHhv() : $paymentOrder->getDepartment()
269
                ->getEmailTreasurer();
270
            $username = sprintf('%s [Confirmation %d]', implode(', ', $emails), $confirm_step);
271
            $this->userProvider->setManualUsername($username, implode(',', $emails));
272
            $em->flush();
273
274
            //Rerender form if it was confirmed, to apply the disabled state
275
            $form = $this->createForm(PaymentOrderConfirmationType::class, null, [
276
                'disabled' => true,
277
            ]);
278
            $this->addFlash('info', 'payment_order.confirmation.already_confirmed');
279
        }
280
281
        return $this->render('PaymentOrder/confirm/confirm.html.twig', [
282
            'entity' => $paymentOrder,
283
            'confirmation_nr' => $confirm_step,
284
            'form' => $form->createView(),
285
            'deletion_form' => $deletion_form->createView(),
286
            'paymentOrder_is_undeletable' => $paymentOrder_is_undeletable,
287
            'already_confirmed' => $already_confirmed,
288
        ]);
289
    }
290
}
291