PollService.php 11 KB
Newer Older
Olivier PEREZ's avatar
Olivier PEREZ committed
1
<?php
2
3
4
5
6
7
/**
 * This software is governed by the CeCILL-B license. If a copy of this license
 * is not distributed with this file, you can obtain one at
 * http://www.cecill.info/licences/Licence_CeCILL-B_V1-en.txt
 *
 * Authors of STUdS (initial project): Guilhem BORGHESI (borghesi@unistra.fr) and Raphaël DROZ
8
 * Authors of Framadate/OpenSondage: Framasoft (https://github.com/framasoft)
9
10
11
12
13
14
15
16
17
18
 *
 * =============================
 *
 * Ce logiciel est régi par la licence CeCILL-B. Si une copie de cette licence
 * ne se trouve pas avec ce fichier vous pouvez l'obtenir sur
 * http://www.cecill.info/licences/Licence_CeCILL-B_V1-fr.txt
 *
 * Auteurs de STUdS (projet initial) : Guilhem BORGHESI (borghesi@unistra.fr) et Raphaël DROZ
 * Auteurs de Framadate/OpenSondage : Framasoft (https://github.com/framasoft)
 */
Olivier PEREZ's avatar
Olivier PEREZ committed
19
20
namespace Framadate\Services;

21
22
use Framadate\Exception\AlreadyExistsException;
use Framadate\Exception\ConcurrentEditionException;
23
use Framadate\Exception\ConcurrentVoteException;
Olivier PEREZ's avatar
Olivier PEREZ committed
24
25
use Framadate\Form;
use Framadate\FramaDB;
26
use Framadate\Repositories\RepositoryFactory;
27
use Framadate\Security\Token;
Olivier PEREZ's avatar
Olivier PEREZ committed
28

Olivier PEREZ's avatar
Olivier PEREZ committed
29
30
class PollService {
    private $connect;
Olivier PEREZ's avatar
Olivier PEREZ committed
31
    private $logService;
Olivier PEREZ's avatar
Olivier PEREZ committed
32

33
    private $pollRepository;
34
    private $slotRepository;
35
    private $voteRepository;
Olivier PEREZ's avatar
Olivier PEREZ committed
36
    private $commentRepository;
Olivier PEREZ's avatar
Olivier PEREZ committed
37

Olivier PEREZ's avatar
Olivier PEREZ committed
38
    function __construct(FramaDB $connect, LogService $logService) {
Olivier PEREZ's avatar
Olivier PEREZ committed
39
        $this->connect = $connect;
Olivier PEREZ's avatar
Olivier PEREZ committed
40
        $this->logService = $logService;
41
        $this->pollRepository = RepositoryFactory::pollRepository();
42
        $this->slotRepository = RepositoryFactory::slotRepository();
43
        $this->voteRepository = RepositoryFactory::voteRepository();
Olivier PEREZ's avatar
Olivier PEREZ committed
44
        $this->commentRepository = RepositoryFactory::commentRepository();
Olivier PEREZ's avatar
Olivier PEREZ committed
45
46
    }

47
48
49
50
51
52
    /**
     * Find a poll from its ID.
     *
     * @param $poll_id int The ID of the poll
     * @return \stdClass|null The found poll, or null
     */
Olivier PEREZ's avatar
Olivier PEREZ committed
53
    function findById($poll_id) {
54
        if (preg_match(POLL_REGEX, $poll_id)) {
55
            return $this->pollRepository->findById($poll_id);
Olivier PEREZ's avatar
Olivier PEREZ committed
56
        }
Olivier PEREZ's avatar
Olivier PEREZ committed
57
58
59
60
61

        return null;
    }

    public function findByAdminId($admin_poll_id) {
62
        if (preg_match(ADMIN_POLL_REGEX, $admin_poll_id)) {
Olivier PEREZ's avatar
Olivier PEREZ committed
63
64
            return $this->pollRepository->findByAdminId($admin_poll_id);
        }
Olivier PEREZ's avatar
Olivier PEREZ committed
65
66
67
68
69

        return null;
    }

    function allCommentsByPollId($poll_id) {
70
        return $this->commentRepository->findAllByPollId($poll_id);
Olivier PEREZ's avatar
Olivier PEREZ committed
71
72
    }

73
    function allVotesByPollId($poll_id) {
74
        return $this->voteRepository->allUserVotesByPollId($poll_id);
Olivier PEREZ's avatar
Olivier PEREZ committed
75
76
    }

Olivier PEREZ's avatar
Olivier PEREZ committed
77
78
    function allSlotsByPoll($poll) {
        $slots = $this->slotRepository->listByPollId($poll->id);
79
        if ($poll->format === 'D') {
80
            $this->sortSlorts($slots);
Olivier PEREZ's avatar
Olivier PEREZ committed
81
        }
82
        return $slots;
Olivier PEREZ's avatar
Olivier PEREZ committed
83
84
    }

85
86
87
88
89
90
    /**
     * @param $poll_id
     * @param $vote_id
     * @param $name
     * @param $choices
     * @param $slots_hash
m's avatar
m committed
91
     * @throws AlreadyExistsException
92
93
     * @throws ConcurrentEditionException
     * @throws ConcurrentVoteException
Thomas Citharel's avatar
CS    
Thomas Citharel committed
94
     * @return bool
95
     */
96
    public function updateVote($poll_id, $vote_id, $name, $choices, $slots_hash) {
m's avatar
m committed
97
        $this->checkVoteConstraints($choices, $poll_id, $slots_hash, $name, $vote_id);
m's avatar
m committed
98
        
99
100
        // Update vote
        $choices = implode($choices);
101
        return $this->voteRepository->update($poll_id, $vote_id, $name, $choices);
Olivier PEREZ's avatar
Olivier PEREZ committed
102
    }
m's avatar
m committed
103
    
104
105
106
107
108
109
110
111
    /**
     * @param $poll_id
     * @param $name
     * @param $choices
     * @param $slots_hash
     * @throws AlreadyExistsException
     * @throws ConcurrentEditionException
     * @throws ConcurrentVoteException
Thomas Citharel's avatar
CS    
Thomas Citharel committed
112
     * @return \stdClass
113
     */
114
    function addVote($poll_id, $name, $choices, $slots_hash) {
m's avatar
m committed
115
        $this->checkVoteConstraints($choices, $poll_id, $slots_hash, $name);
m's avatar
m committed
116
        
117
        // Insert new vote
Olivier PEREZ's avatar
Olivier PEREZ committed
118
        $choices = implode($choices);
Antonin's avatar
Antonin committed
119
        $token = $this->random(16);
Olivier PEREZ's avatar
Olivier PEREZ committed
120
        return $this->voteRepository->insert($poll_id, $name, $choices, $token);
Olivier PEREZ's avatar
Olivier PEREZ committed
121
122
    }

123
    function addComment($poll_id, $name, $comment) {
Olivier PEREZ's avatar
Olivier PEREZ committed
124
125
        if ($this->commentRepository->exists($poll_id, $name, $comment)) {
            return true;
126
        }
m's avatar
m committed
127
128
        
        return $this->commentRepository->insert($poll_id, $name, $comment);
129
130
    }

131
132
    /**
     * @param Form $form
133
     * @return array
134
135
136
     */
    function createPoll(Form $form) {
        // Generate poll IDs, loop while poll ID already exists
137
138
139
140
141
142
143
144
145
146
147
148

        if (empty($form->id)) { // User want us to generate an id for him
            do {
                $poll_id = $this->random(16);
            } while ($this->pollRepository->existsById($poll_id));
            $admin_poll_id = $poll_id . $this->random(8);
        } else { // User have choosen the poll id
            $poll_id = $form->id;
            do {
                $admin_poll_id = $this->random(24);
            } while ($this->pollRepository->existsByAdminId($admin_poll_id));
        }
149
150
151

        // Insert poll + slots
        $this->pollRepository->beginTransaction();
Antonin's avatar
Antonin committed
152
        $this->pollRepository->insertPoll($poll_id, $admin_poll_id, $form);
153
154
155
156
157
        $this->slotRepository->insertSlots($poll_id, $form->getChoices());
        $this->pollRepository->commit();

        $this->logService->log('CREATE_POLL', 'id:' . $poll_id . ', title: ' . $form->title . ', format:' . $form->format . ', admin:' . $form->admin_name . ', mail:' . $form->admin_mail);

158
        return [$poll_id, $admin_poll_id];
Olivier PEREZ's avatar
Olivier PEREZ committed
159
160
    }

161
162
163
164
    public function findAllByAdminMail($mail) {
        return $this->pollRepository->findAllByAdminMail($mail);
    }

Thomas Citharel's avatar
Thomas Citharel committed
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
    /**
     * @param array $votes
     * @param \stdClass $poll
     * @return array
     */
    public function computeBestChoices($votes, $poll) {
        if (0 === count($votes)) {
           return $this->computeEmptyBestChoices($poll);
        }
        $result = ['y' => [], 'inb' => []];

        // if there are votes
        foreach ($votes as $vote) {
            $choices = str_split($vote->choices);
            foreach ($choices as $i => $choice) {
                if (!isset($result['y'][$i])) {
                    $result['inb'][$i] = 0;
                    $result['y'][$i] = 0;
                }
                if ($choice === "1") {
                    $result['inb'][$i]++;
                }
                if ($choice === "2") {
                    $result['y'][$i]++;
Olivier PEREZ's avatar
Olivier PEREZ committed
189
190
191
                }
            }
        }
Olivier PEREZ's avatar
Olivier PEREZ committed
192

Olivier PEREZ's avatar
Olivier PEREZ committed
193
194
195
196
        return $result;
    }

    function splitSlots($slots) {
197
        $splitted = [];
Olivier PEREZ's avatar
Olivier PEREZ committed
198
199
        foreach ($slots as $slot) {
            $obj = new \stdClass();
200
201
            $obj->day = $slot->title;
            $obj->moments = explode(',', $slot->moments);
Olivier PEREZ's avatar
Olivier PEREZ committed
202
203
204

            $splitted[] = $obj;
        }
Olivier PEREZ's avatar
Olivier PEREZ committed
205

Olivier PEREZ's avatar
Olivier PEREZ committed
206
207
208
        return $splitted;
    }

209
210
211
212
213
214
215
216
217
218
    /**
     * @param $slots array The slots to hash
     * @return string The hash
     */
    public function hashSlots($slots) {
        return md5(array_reduce($slots, function($carry, $item) {
            return $carry . $item->id . '@' . $item->moments . ';';
        }));
    }

Olivier PEREZ's avatar
Olivier PEREZ committed
219
    function splitVotes($votes) {
220
        $splitted = [];
Olivier PEREZ's avatar
Olivier PEREZ committed
221
222
        foreach ($votes as $vote) {
            $obj = new \stdClass();
223
224
            $obj->id = $vote->id;
            $obj->name = $vote->name;
Antonin's avatar
Antonin committed
225
            $obj->uniqId = $vote->uniqId;
226
            $obj->choices = str_split($vote->choices);
Olivier PEREZ's avatar
Olivier PEREZ committed
227
228
229

            $splitted[] = $obj;
        }
Olivier PEREZ's avatar
Olivier PEREZ committed
230

Olivier PEREZ's avatar
Olivier PEREZ committed
231
232
        return $splitted;
    }
Olivier PEREZ's avatar
Olivier PEREZ committed
233

234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
    /**
     * @return int The max timestamp allowed for expiry date
     */
    public function maxExpiryDate() {
        global $config;
        return time() + (86400 * $config['default_poll_duration']);
    }

    /**
     * @return int The min timestamp allowed for expiry date
     */
    public function minExpiryDate() {
        return time() + 86400;
    }

249
250
251
252
253
254
255
256
257
258
    /**
     * @return mixed
     */
    public function sortSlorts(&$slots) {
        uasort($slots, function ($a, $b) {
            return $a->title > $b->title;
        });
        return $slots;
    }

Thomas Citharel's avatar
CS    
Thomas Citharel committed
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
    /**
     * @param \stdClass $poll
     * @return array
     */
    private function computeEmptyBestChoices($poll)
    {
        $result = ['y' => [], 'inb' => []];
        // if there is no votes, calculates the number of slot

        $slots = $this->allSlotsByPoll($poll);

        if ($poll->format === 'A') {
            // poll format classic

            for ($i = 0; $i < count($slots); $i++) {
                $result['y'][] = 0;
                $result['inb'][] = 0;
            }
        } else {
            // poll format date

            $slots = $this->splitSlots($slots);

            foreach ($slots as $slot) {
                for ($i = 0; $i < count($slot->moments); $i++) {
                    $result['y'][] = 0;
                    $result['inb'][] = 0;
                }
            }
        }
        return $result;
    }

292
293
294
    private function random($length) {
        return Token::getToken($length);
    }
m's avatar
m committed
295
296
297
298
299
300
    
    /**
     * @param $choices
     * @param $poll_id
     * @param $slots_hash
     * @param $name
m's avatar
m committed
301
     * @param string $vote_id
m's avatar
m committed
302
303
304
305
     * @throws AlreadyExistsException
     * @throws ConcurrentVoteException
     * @throws ConcurrentEditionException
     */
m's avatar
m committed
306
    private function checkVoteConstraints($choices, $poll_id, $slots_hash, $name, $vote_id = FALSE) {
m's avatar
m committed
307
        // Check if vote already exists with the same name
m's avatar
m committed
308
        if (FALSE === $vote_id) {
m's avatar
m committed
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
        	$exists = $this->voteRepository->existsByPollIdAndName($poll_id, $name);
        } else {
        	$exists = $this->voteRepository->existsByPollIdAndNameAndVoteId($poll_id, $name, $vote_id);
        }
        
        if ($exists) {
            throw new AlreadyExistsException();
        }
        
        $poll = $this->findById($poll_id);
        
        // Check that no-one voted in the meantime and it conflicts the maximum votes constraint
        $this->checkMaxVotes($choices, $poll, $poll_id);
        
        // Check if slots are still the same
        $this->checkThatSlotsDidntChanged($poll, $slots_hash);
    }
    
327
328
329
    /**
     * This method checks if the hash send by the user is the same as the computed hash.
     *
330
     * @param $poll /stdClass The poll
331
332
333
     * @param $slots_hash string The hash sent by the user
     * @throws ConcurrentEditionException Thrown when hashes are differents
     */
334
335
    private function checkThatSlotsDidntChanged($poll, $slots_hash) {
        $slots = $this->allSlotsByPoll($poll);
336
337
338
339
        if ($slots_hash !== $this->hashSlots($slots)) {
            throw new ConcurrentEditionException();
        }
    }
340
341
342
343
344
345
346
347
348
349

    /**
     * This method checks if the votes doesn't conflicts the maximum votes constraint
     *
     * @param $user_choice
     * @param \stdClass $poll
     * @param string $poll_id
     * @throws ConcurrentVoteException
     */
    private function checkMaxVotes($user_choice, $poll, $poll_id) {
350
351
352
353
        $votes = $this->allVotesByPollId($poll_id);
        if (count($votes) <= 0) {
            return;
        }
m's avatar
m committed
354
        $best_choices = $this->computeBestChoices($votes, $poll);
355
356
        foreach ($best_choices['y'] as $i => $nb_choice) {
            // if for this option we have reached maximum value and user wants to add itself too
m's avatar
m committed
357
            if ($poll->ValueMax !== null && $nb_choice >= $poll->ValueMax && $user_choice[$i] === "2") {
358
359
360
361
                throw new ConcurrentVoteException();
            }
        }
    }
Olivier PEREZ's avatar
Olivier PEREZ committed
362
}