Completed
Push — master ( 01615d...a88e90 )
by Konstantinos
04:19
created

MessageController::getErrorMessage()   A

Complexity

Conditions 4
Paths 5

Size

Total Lines 14
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 20
Metric Value
dl 0
loc 14
ccs 0
cts 0
cp 0
rs 9.2
cc 4
eloc 7
nc 5
nop 1
crap 20
1
<?php
2
3
use BZIon\Event\ConversationAbandonEvent;
4
use BZIon\Event\ConversationJoinEvent;
5
use BZIon\Event\ConversationKickEvent;
6
use BZIon\Event\ConversationRenameEvent;
7
use BZIon\Event\Events;
8
use BZIon\Event\NewMessageEvent;
9
use BZIon\Form\Creator\ConversationFormCreator;
10
use BZIon\Form\Creator\ConversationInviteFormCreator;
11
use BZIon\Form\Creator\ConversationRenameFormCreator;
12
use BZIon\Form\Creator\MessageFormCreator;
13
use BZIon\Form\Creator\MessageSearchFormCreator;
14
use BZIon\Search\MessageSearch;
15
use Symfony\Component\Form\Form;
16
use Symfony\Component\HttpFoundation\JsonResponse;
17
use Symfony\Component\HttpFoundation\RedirectResponse;
18
use Symfony\Component\HttpFoundation\Request;
19
20
class MessageController extends JSONController
21
{
22 1
    public function setup()
23
    {
24 1
        $this->requireLogin();
25 1
    }
26
27 1
    protected function prepareTwig()
28
    {
29 1
        $currentPage = $this->getRequest()->query->get('page', 1);
30
31 1
        $query = $this->getQueryBuilder('Conversation')
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class QueryBuilder as the method forPlayer() does only exist in the following sub-classes of QueryBuilder: ConversationQueryBuilder, MessageQueryBuilder. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different sub-classes of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
32 1
            ->forPlayer($this->getMe())
33 1
            ->sortBy('last_activity')->reverse()
34 1
            ->limit(5)->fromPage($currentPage);
35
36 1
        $creator = new MessageSearchFormCreator();
37 1
        $searchForm = $creator->create();
38
39 1
        $twig = $this->container->get('twig');
40 1
        $twig->addGlobal("conversations", $query->getModels());
41 1
        $twig->addGlobal("currentPage", $currentPage);
42 1
        $twig->addGlobal("totalPages", $query->countPages());
43 1
        $twig->addGlobal("searchForm", $searchForm->createView());
44
45 1
        return $twig;
46
    }
47
48
    public function listAction()
49
    {
50
        return array();
51
    }
52
53 1
    public function composeAction(Player $me, Request $request)
54
    {
55 1
        if (!$me->hasPermission(Permission::SEND_PRIVATE_MSG)) {
56 1
            throw new ForbiddenException("You are not allowed to send messages");
57
        }
58
59 1
        $creator = new ConversationFormCreator($me);
60 1
        $form = $creator->create()->handleRequest($request);
61
62 1
        if ($form->isSubmitted()) {
63 1
            if ($form->isValid()) {
64 1
                $subject = $form->get('Subject')->getData();
65 1
                $content = $form->get('Message')->getData();
66 1
                $recipients = $form->get('Recipients')->getData();
67
68 1
                $conversation_to = Conversation::createConversation($subject, $me->getId(), $recipients);
69 1
                $message = $conversation_to->sendMessage($me, $content);
70
71 1
                $event = new NewMessageEvent($message, true);
72 1
                $this->dispatch(Events::MESSAGE_NEW, $event);
73
74 1
                if ($this->isJson()) {
75
                    return new JsonResponse(array(
76
                        'success' => true,
77
                        'message' => 'Your message was sent successfully',
78
                        'id'      => $conversation_to->getId()
79
                    ));
80
                } else {
81 1
                    return new RedirectResponse($conversation_to->getUrl());
82
                }
83 1
            } elseif ($this->isJson()) {
84 1
                throw new BadRequestException($this->getErrorMessage($form));
85
            }
86
        } else {
87
            // Load the list of recipients from the URL
88 1
            if ($request->query->has('recipients')) {
89
                $form->get('Recipients')->setData($this->decompose(
90
                    $request->query->get('recipients'),
91
                    array('Player', 'Team')
92
                ));
93
            }
94
        }
95
96 1
        return array("form" => $form->createView());
97
    }
98
99 1
    public function showAction(Conversation $conversation, Player $me, Request $request)
100
    {
101 1
        $this->assertCanParticipate($me, $conversation);
102 1
        $conversation->markReadBy($me->getId());
103
104 1
        $form = $this->showMessageForm($conversation, $me);
105 1
        $inviteForm = $this->showInviteForm($conversation, $me);
106 1
        $renameForm = $this->showRenameForm($conversation, $me);
107
108 1
        $messages = $this->getQueryBuilder('AbstractMessage')
109 1
                  ->where('conversation')->is($conversation)
110 1
                  ->sortBy('time')->reverse()
111 1
                  ->limit(10)->fromPage($request->query->get('page', 1))
112 1
                  ->startAt($request->query->get('end'))
113 1
                  ->endAt($request->query->get('start'))
114 1
                  ->getModels();
115
116
        // Hide the details (author, timestamp) of the first message if they're
117
        // already shown in the previous message (useful for AJAX calls)
118 1
        if($request->query->getBoolean('hideFirstDetails')) {
119
            $previousMessage = Message::get($request->query->get('end'));
120
        } else {
121 1
            $previousMessage = null;
122
        }
123
124
        $params = array(
125 1
            "form"         => $form->createView(),
126 1
            "inviteForm"   => $inviteForm->createView(),
127 1
            "renameForm"   => $renameForm->createView(),
128 1
            "conversation" => $conversation,
129 1
            "messages"     => $messages,
130 1
            "previousMessage" => $previousMessage
131
        );
132
133 1
        if ($request->query->has('nolayout')) {
134
            if ($request->query->getBoolean('reviewLastDetails')) {
135
                // An AJAX call has asked us to check if details (author,
136
                // timestamp) will need to be shown for the next message
137
138
                $nextMessage = Message::get($request->query->get('start'));
139
                $lastMessage = reset($messages);
140
141
                if ($lastMessage !== false
142
                    && $lastMessage->isMessage()
143
                    && $nextMessage->isMessage()
144
                    && $lastMessage->getTimestamp()->isSameDay($nextMessage->getTimestamp())
145
                    && $lastMessage->getAuthor()->isSameAs($nextMessage->getAuthor())
146
                ) {
147
                    $hideLastDetails = true;
148
                } else {
149
                    $hideLastDetails = false;
150
                }
151
152
                // Add $hideLastDetails to the list of JSON parameters
153
                $this->attributes->set('hideLastDetails', $hideLastDetails);
154
            }
155
156
            // Don't show the layout so that ajax can just load the messages
157
            return $this->render('Message/messages.html.twig', $params);
158 1
        } else {
159
            return $params;
160
        }
161
    }
162
163
    public function leaveAction(Player $me, Conversation $conversation)
164
    {
165
        if (!$conversation->isMember($me, $distinct = true)) {
166
            throw new ForbiddenException("You are not a member of this discussion.");
167
        } elseif ($conversation->getCreator()->isSameAs($me)) {
168
            throw new ForbiddenException("You can't abandon the conversation you started!");
169
        }
170
171
        return $this->showConfirmationForm(function () use ($conversation, $me) {
172
            $conversation->removeMember($me);
173
174
            $event = new ConversationAbandonEvent($conversation, $me);
175
            Service::getDispatcher()->dispatch(Events::CONVERSATION_ABANDON, $event);
176
177
            return new RedirectResponse(Service::getGenerator()->generate('message_list'));
178
        },  "Are you sure you want to abandon this conversation?",
179
            "You will no longer receive messages from this conversation", "Leave");
180
    }
181
182
    public function teamLeaveAction(Player $me, Conversation $conversation)
183
    {
184
        $team = $me->getTeam();
185
186
        if (!$me->canEdit($team)) {
0 ignored issues
show
Bug introduced by
It seems like $team defined by $me->getTeam() on line 184 can be null; however, Player::canEdit() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
187
            throw new ForbiddenException("You are not allowed to remove your team from this conversation.");
188
        } elseif (!$conversation->isMember($team)) {
0 ignored issues
show
Bug introduced by
It seems like $team defined by $me->getTeam() on line 184 can be null; however, Conversation::isMember() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
189
            throw new ForbiddenException("That team is not participating in this conversation.");
190
        }
191
192
        return $this->showConfirmationForm(function () use ($conversation, $team) {
193
            $conversation->removeMember($team);
194
195
            $event = new ConversationAbandonEvent($conversation, $team);
196
            Service::getDispatcher()->dispatch(Events::CONVERSATION_ABANDON, $event);
197
198
            return new RedirectResponse($conversation->getURL());
199
        },  "Are you sure you want to remove {$team->getEscapedName()} from this conversation?",
200
            "Your team is no longer participating in that conversation.", "Remove team");
201
    }
202
203
    public function kickAction(Conversation $conversation, Player $me, $type, $member, Player $me)
0 ignored issues
show
Bug introduced by
The parameter $me is used multiple times.
Loading history...
204
    {
205 1
        $this->assertCanEdit($me, $conversation, "You are not allowed to kick a member off that discussion!");
206
207 1
        if (strtolower($type) === 'player') {
208
            $member = Player::fetchFromSlug($member);
209 1
        } elseif (strtolower($type) === 'team') {
210
            $member = Team::fetchFromSlug($member);
211
        } else {
212
            throw new BadRequestException("Unrecognized member type.");
213
        }
214 1
215 1
        if ($conversation->isCreator($member->getId())) {
216
            throw new ForbiddenException("You can't leave your own conversation.");
217
        }
218 1
219
        if (!$conversation->isMember($member)) {
0 ignored issues
show
Documentation introduced by
$member is of type object<AliasModel>|null, but the function expects a object<Player>|object<Team>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
220
            throw new ForbiddenException("The specified player is not a member of this conversation.");
221
        }
222
223
        return $this->showConfirmationForm(function () use ($conversation, $member, $me) {
224
            $conversation->removeMember($member);
225
226
            $event = new ConversationKickEvent($conversation, $member, $me);
227
            Service::getDispatcher()->dispatch(Events::CONVERSATION_KICK, $event);
228 1
229
            return new RedirectResponse($conversation->getUrl());
230 1
        },  "Are you sure you want to kick {$member->getEscapedName()} from the discussion?",
231 1
            "{$member->getName()} has been kicked from the conversation", "Kick");
232
    }
233 1
234
    public function searchAction(Player $me, Request $request)
235
    {
236
        $query = $request->query->get('q');
237
238
        if (strlen($query) < 3 && !$this->isDebug()) {
239
            // TODO: Find a better error message
240
            throw new BadRequestException('The search term you have provided is too short');
241
        }
242
243
        $search  = new MessageSearch($this->getQueryBuilder(), $me);
0 ignored issues
show
Compatibility introduced by
$this->getQueryBuilder() of type object<QueryBuilder> is not a sub-type of object<MessageQueryBuilder>. It seems like you assume a child class of the class QueryBuilder to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
244
        $results = $search->search($query);
245
246
        return array(
247
            'messages' => $results
248
        );
249
    }
250
251
    /**
252
     * @param Conversation  $conversation
253
     * @param Player $me
254
     *
255 1
     * @return $this|Form|\Symfony\Component\Form\FormInterface
256
     */
257
    private function showInviteForm($conversation, $me)
258
    {
259
        $creator = new ConversationInviteFormCreator($conversation);
260
        $form = $creator->create()->handleRequest($this->getRequest());
261
262 1
        if ($form->isValid()) {
263
            $this->assertCanEdit($me, $conversation);
264 1
            $invitees = array();
265 1
266
            foreach ($form->get('players')->getData() as $player) {
267 1
                if (!$conversation->isMember($player, $distinct = true)) {
268
                    $conversation->addMember($player);
269
                    $invitees[] = $player;
270
                }
271
            }
272
273
            if (!empty($invitees)) {
274
                $event = new ConversationJoinEvent($conversation, $invitees);
275
                Service::getDispatcher()->dispatch(Events::CONVERSATION_JOIN, $event);
276
            }
277
278
            $this->getFlashBag()->add('success', "The conversation has been updated");
279 1
280
            // Reset the form fields
281
            return $creator->create();
282
        }
283
284
        return $form;
285
    }
286 1
287
    /**
288
     * @param Conversation  $conversation
289 1
     * @param Player $me
290 1
     */
291
    private function showRenameForm($conversation, $me)
292
    {
293
        $creator = new ConversationRenameFormCreator($conversation);
294 1
        $form = $creator->create()->handleRequest($this->getRequest());
295 1
296
        if ($form->isValid()) {
297 1
            $this->assertCanEdit($me, $conversation);
298
299 1
            $newName = $form->get('subject')->getData();
300 1
301
            $event = new ConversationRenameEvent($conversation, $conversation->getSubject(), $newName, $me);
302
            $conversation->setSubject($newName);
303
            Service::getDispatcher()->dispatch(Events::CONVERSATION_RENAME, $event);
304 1
305
            $this->getFlashBag()->add('success', "The conversation has been updated");
306
        }
307
308
        return $form;
309
    }
310
311
    /**
312
     * @param Conversation  $conversation
313
     * @param Player $me
314
     */
315
    private function showMessageForm($conversation, $me)
316
    {
317
        // Create the form to send a message to the conversation
318 1
        $creator = new MessageFormCreator($conversation);
319
        $form = $creator->create();
320
321 1
        // Keep a cloned version so we can come back to it later, if we need
322
        // to reset the fields of the form
323
        $cloned = clone $form;
324 1
        $form->handleRequest($this->getRequest());
325
326
        if ($form->isValid()) {
327
            // The player wants to send a message
328
            $this->sendMessage($me, $conversation, $form, $cloned);
329
        } elseif ($form->isSubmitted() && $this->isJson()) {
330
            throw new BadRequestException($this->getErrorMessage($form));
331
        }
332
333
        return $form;
334
    }
335
336
    /**
337
     * Make sure that a player can participate in a conversation
338 1
     *
339
     * Throws an exception if a player is not an admin or a member of that conversation
340 1
     * @todo Permission for spying on other people's conversations?
341 1
     * @param  Player        $player  The player to test
342
     * @param  Conversation         $conversation   The message conversation
343
     * @param  string        $message The error message to show
344 1
     * @throws HTTPException
345 1
     * @return void
346
     */
347 1
    private function assertCanParticipate(Player $player, Conversation $conversation,
348
        $message = "You are not allowed to participate in that discussion"
349
    ) {
350 1
        if (!$conversation->isMember($player)) {
351
            throw new ForbiddenException($message);
352
        }
353 1
    }
354
355
    /**
356 1
     * Sends a message to a conversation
357 1
     *
358 1
     * @param  Player        $from   The sender
359
     * @param  Conversation         $to     The conversation that will receive the message
360
     * @param  Form          $form   The message's form
361
     * @param  Form          $form   The form before it handled the request
362
     * @param  Form          $cloned
363
     * @throws HTTPException Thrown if the user doesn't have the
364
     *                              SEND_PRIVATE_MSG permission
365
     * @return void
366
     */
367
    private function sendMessage(Player $from, Conversation $to, &$form, $cloned)
368
    {
369
        if (!$from->hasPermission(Permission::SEND_PRIVATE_MSG)) {
370
            throw new ForbiddenException("You are not allowed to send messages");
371
        }
372
373
        $message = $form->get('message')->getData();
374
        $message = $to->sendMessage($from, $message);
375
376
        $this->getFlashBag()->add('success', "Your message was sent successfully");
377
378
        // Let javascript know the message's ID
379
        $this->attributes->set('id', $message->getId());
380
381
        // Reset the form
382
        $form = $cloned;
383
384
        // Notify everyone that we sent a new message
385
        $event = new NewMessageEvent($message, false);
386
        $this->dispatch(Events::MESSAGE_NEW, $event);
387
    }
388
389
    /**
390
     * @return string|null
391
     */
392
    private function getErrorMessage(Form $form)
393
    {
394
        foreach ($form->all() as $child) {
395
            foreach ($child->getErrors() as $error) {
396
                return $error->getMessage();
397
            }
398
        }
399
400
        foreach ($form->getErrors() as $error) {
401
            return $error->getMessage();
402
        }
403
404
        return "Unknown Error";
405
    }
406
407
    /**
408
     * Make sure that a player can edit a conversation
409
     *
410
     * Throws an exception if a player is not an admin or the leader of a team
411
     * @param  Player        $player  The player to test
412
     * @param  Conversation         $conversation   The team
413
     * @param  string        $message The error message to show
414
     * @throws HTTPException
415
     * @return void
416
     */
417
    private function assertCanEdit(Player $player, Conversation $conversation, $message = "You are not allowed to edit the discussion")
418
    {
419
        if ($conversation->getCreator()->getId() != $player->getId()) {
420
            throw new ForbiddenException($message);
421
        }
422
    }
423
}
424