<?php
namespace Bait\PollBundle\Controller;
use Bait\PollBundle\Entity\Answer;
use Bait\PollBundle\Entity\Question;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Symfony\Component\HttpFoundation\Cookie;
use Bait\PollBundle\Entity\Poll;
use Bait\PollBundle\Entity\UserAnswer;
use AppBundle\Entity\UserExtraData;
use Bait\PollBundle\Entity\IpAddress;
use Bait\PollBundle\Entity\UserAnswerGroup;
use Bait\PollBundle\Event\PollSubmitCompletedEvent;
class PollController extends Controller
{
public function renderPollAction($id) {
$request = Request::createFromGlobals();
$host = $this->container->getParameter('site_url');
/** @var EntityManagerInterface $em */
$em = $this->getDoctrine()->getManager();
/** @var Poll $poll */
$poll = $em->getRepository('BaitPollBundle:Poll')
->findOneById($id);
$user = $this->getUser();
$viewData['poll'] = $poll;
$viewData['user'] = $user;
$viewData['errors'] = [];
// Personalized error messages for specific inputs
$viewData['inputErrors'] = [];
// check if poll exists (fixes case when article content has reference to deleted poll)
if (empty($viewData['poll'])) {
return new Response('', Response::HTTP_NO_CONTENT);
}
// check answerability of poll
// if user is logged in, and poll is not multi entry, check by existing user answer
if ($user && !$poll->getMultiEntry()) {
$alreadyAnswered = $em->getRepository('BaitPollBundle:UserAnswerGroup')->findOneBy([
'user' => $user->getId(),
'poll' => $poll,
]);
// if user is not present or the poll is multi entry, check by cookie
} else {
$cookies = $request->cookies;
$alreadyAnswered = $cookies->has('answered_poll_' . $id);
}
$viewData['alreadyAnswered'] = $alreadyAnswered;
$viewData['recaptchaSiteKey'] = $this->container->getParameter('recaptcha_site_key');
if (empty($poll->getVotesLimit())) {
$votesLimitExceeded = false;
} else {
$votesLimitExceeded = $this->getPollAnswerCounts($poll) >= $poll->getVotesLimit();
}
if ($votesLimitExceeded) {
$poll->setMsgEnded('Maximálny počet hlasov bol naplnený.');
$poll->setDateEnd(new \DateTime(date('c', strtotime('-1 hour'))));
}
$dateEnd = $poll->getDateEnd();
if ($alreadyAnswered || ($dateEnd != null && (int)$dateEnd->format('U') < time())) {
$ongoingResults = $this->getOngoingResults($id);
$viewData['ongoingResultQuestions'] = $ongoingResults['ongoingResultQuestions'];
$viewData['ongoingResultAnswers'] = $ongoingResults['ongoingResultAnswers'];
}
if ($request->isMethod('POST') && !$alreadyAnswered && !$votesLimitExceeded && $_POST['submit'] == $id) {
if ($poll->getMultiEntry() && $poll->getMultiEntryInterval() == 'without_limit') {
$recaptchaName = 'g-recaptcha-response';
$recaptchaToken = $_POST[$recaptchaName];
if (empty($recaptchaToken)) {
$errorMsg = $this->get('translator')->trans('Please check the reCAPTCHA form');
return $this->handleError($viewData, $errorMsg, $recaptchaName);
}
$secretKey = $this->container->getParameter('recaptcha_secret_key');
$verificationUrl = 'https://www.google.com/recaptcha/api/siteverify?secret=' . urlencode($secretKey) . '&response=' . urlencode($recaptchaToken);
$verificationResponse = file_get_contents($verificationUrl);
$verificationResponse = json_decode($verificationResponse, true);
if (!$verificationResponse['success']) {
$errorMsg = $this->get('translator')->trans('reCAPTCHA verification failed!');
return $this->handleError($viewData, $errorMsg, $recaptchaName);
}
}
$data = $request->request->all();
$data = array_merge($data, $_FILES);
$isQuiz = count($poll->getResults()) != 0; // if poll has mapped results, it's a quiz
$quizWeights = [];
// validate if all required fields were set
/** @var Answer[] $requireds */
$requireds = $em->getRepository('BaitPollBundle:Answer')
->createQueryBuilder('a')
->join('a.question', 'q')
->join('q.poll', 'p')
->where('p.id = :pollId')
->andWhere('a.required = true')
->setParameter('pollId', $id)
->getQuery()
->getResult();
$questionIds = [];
foreach($data as $a => $val) {
$keys = explode('-', $a);
// this means question was NOT answered
if (array_key_exists('3', $keys) && empty($val)) {
$questionIds[] = [
'qid' => (int)$keys[3],
'name' => $a,
];
}
}
foreach($requireds as $r) {
$qid = $r->getQuestion()->getId();
$qt = $r->getQuestion()->getTitle();
$match = array_filter($questionIds, function($item) use ($qid) {
return $item['qid'] === $qid;
});
if (!empty($match)) {
$inputName = reset($match)['name'];
$errorMsg = $qt . ' ' . $this->get('translator')->trans('is a required field');
return $this->handleError($viewData, $errorMsg, $inputName);
}
}
$ipAddress = $em->getRepository('BaitPollBundle:IpAddress')->findOneBy(['ip' => $request->getClientIp()]);
if (empty($ipAddress)) {
$ipAddress = new IpAddress();
$ipAddress->setIp($request->getClientIp());
$em->persist($ipAddress);
$em->flush();
}
$userAnswerGroup = new UserAnswerGroup();
$userAnswerGroup->setPoll($poll);
if ($user) {
$userAnswerGroup->setUser($user->getId());
}
$userAnswerGroup->setIpReferenceId($ipAddress->getId());
$em->persist($userAnswerGroup);
foreach($data as $inputName => $value) {
if ($inputName == 'submit') continue;
if ($inputName == 'g-recaptcha-response') continue;
if (empty($value)) continue;
$keys = explode('-', $inputName);
$pollId = $keys[1];
$questionId = $keys[3];
$answerType = $keys[4];
$userAnswer = new UserAnswer();
/** @var Question $question */
$question = $em->getReference('BaitPollBundle:Question', $questionId);
$userAnswer->setUserAnswerGroup($userAnswerGroup);
$userAnswer->setQuestion($question);
$userAnswer->setAnswerType($answerType);
$userAnswer->setCreated(new \DateTime("now"));
// FR-91 get email address for eshop confirmation email - FR poll #145
if (strpos($host, 'funradio') !== false && $pollId == 145 && filter_var($value, FILTER_VALIDATE_EMAIL)) {
$email = $value;
}
if ($answerType == 'phone') {
$answer = $em->getReference('BaitPollBundle:Answer', $value);
$normalized = str_replace(' ', '', $value);
// Validate E.164 phone number
if (!preg_match('/^\+\d{7,15}$/', $normalized)) {
$errorMsg = $this->get('translator')->trans('Invalid phone number format');
return $this->handleError($viewData, $errorMsg, $inputName);
}
}
if ($answerType == 'text') {
/** @var Answer $answer */
$answerId = $keys[5];
$answer = $em->getReference('BaitPollBundle:Answer', $answerId);
// if answer.answer is defined, check if matches submitted value
if (!empty($answer->getAnswer())) {
$answerSlug = $this->createSlug($answer->getAnswer());
$valueSlug = $this->createSlug($value);
if ($answerSlug != $valueSlug) {
$errorMsg = $this->get('translator')->trans('Incorrectly answered question');
return $this->handleError($viewData, $errorMsg, $inputName);
}
}
}
// handle various answer types
if ($answerType == 'radio' || $answerType == 'checkbox' || $answerType == 'dropdown') {
/** @var Answer $answer */
$answer = $em->getReference('BaitPollBundle:Answer', $value);
// if quiz, mark weight of user's answer
if ($isQuiz && $answer->getAnswerValue()) {
$quizWeights[] = $answer->getAnswerValue();
}
$userAnswer->setAnswer($answer);
$answerValue = $answer->getAnswer();
} else if ($answerType == 'upload') {
$file = new UploadedFile($value["tmp_name"], $value["name"], $value["type"], $value["size"], $value["error"]);
if(!$file->isValid()) {
continue; // optional file upload field
}
if ($value["size"]/1000/1000 > 10) { // @todo: pull out to config
$errorMsg = $this->get('translator')->trans('File too large');
return $this->handleError($viewData, $errorMsg, $inputName);
}
// This is necessary because Safari may label recorded audio files (.mp4) as 'application/octet-stream' by default
if ($file->getMimeType() === 'application/octet-stream') {
// Attempt to guess based on the file's original name or default to bin
$extension = $file->getClientOriginalExtension() ?: 'bin';
} else {
$extension = $file->guessExtension();
}
$fileName = md5(uniqid()).'.'.$extension;
$allowedTypes = [
// images
"image/png",
"image/jpeg",
"image/jpg",
"image/gif",
"image/heic",
// multimedia
"audio/mpeg", // .mp3 as per RFC
"audio/mp3", // .mp3 Firefox, see https://stackoverflow.com/a/28021591/282325
"audio/wav",
"audio/x-wav",
"application/ogg",
"audio/ogg",
"audio/mp4",
"audio/x-m4a",
"audio/m4a",
"audio/webm",
// documents
"application/pdf",
"application/msword", // .doc
"application/vnd.openxmlformats-officedocument.wordprocessingml.document", // .docx
];
if (!in_array($value["type"], $allowedTypes)) {
$errorMsg = $this->get('translator')->trans('File type not allowed');
return $this->handleError($viewData, $errorMsg, $inputName);
}
$file->move(
$this->getParameter('upload_path') . '/' . 'poll' . '/' . $pollId,
$fileName
);
$userAnswer->setValue($fileName);
$answerValue = $fileName;
} else {
$userAnswer->setValue($value);
$answerValue = $value;
}
if ($question->getSaveToUser()) {
$extras = new UserExtraData();
$extras->setUser($viewData['user']);
$extras->setKey($question->getTitle());
$extras->setValue($answerValue);
$em->persist($extras);
}
$em->persist($userAnswer);
$viewData['alreadyAnswered'] = $userAnswer;
}
$em->flush();
$viewData['answerSaved'] = true;
$dispatcher = $this->get('event_dispatcher');
$event = new PollSubmitCompletedEvent($poll, $user);
$dispatcher->dispatch(PollSubmitCompletedEvent::NAME, $event);
// handle quiz results
if ($isQuiz && $poll->getResultsSummary() == 'most_frequent') {
$values = array_count_values($quizWeights);
arsort($values);
$values = array_keys($values);
$viewData['result'] = $em->getRepository('BaitPollBundle:Result')
->findOneBy(['poll' => $id, 'weight' => $values[0]]);
}
if ($isQuiz && $poll->getResultsSummary() == 'sum') {
$sum = array_sum($quizWeights);
$result = $em->getRepository('BaitPollBundle:Result')
->findOneBy(['poll' => $id, 'weight' => $sum]);
if (empty($result)) {
$result = $em->getRepository('BaitPollBundle:Result')
->findOneBy(['poll' => $id, 'weight' => 'default']);
}
$viewData['result'] = $result;
}
// set a cookie and handle repeated poll entering
$pollMultiEntry = $poll->getMultiEntry();
$pollMultiEntryInterval = $poll->getMultiEntryInterval();
$pollResetHour = $poll->getMultiEntryResetHour();
$cookieReset = strtotime('now + 10 years');
if ($pollMultiEntry) {
switch ($pollMultiEntryInterval) {
case 'once_per_day':
$day = date("H") > $pollResetHour ? 'tomorrow' : 'today';
$cookieReset = strtotime($day . ' ' . $pollResetHour . ':00');
break;
case 'once_per_hour':
// PHP treats 'today 24:00' as 'tomorrow 00:00'
$cookieReset = strtotime('today ' . (date("H") + 1) . ':00');
break;
case 'once_per_week':
$cookieReset = strtotime('next monday');
break;
}
}
$ongoingResults = $this->getOngoingResults($id);
$viewData['ongoingResultQuestions'] = $ongoingResults['ongoingResultQuestions'];
$viewData['ongoingResultAnswers'] = $ongoingResults['ongoingResultAnswers'];
// FR-91 send confirmation and notification email for FR poll #145
if (strpos($host, 'funradio') !== false && $poll->getId() === 145) {
if (isset($email)) {
$this->sendEmail('confirmation', $email);
}
$this->sendEmail('notification', $this->getParameter('eshop_notification_email'));
}
$cookie = new Cookie('answered_poll_' . $id, 'true', $cookieReset);
$response = $this->render('BaitPollBundle:Poll:poll_results.html.twig', $viewData);
if (!($poll->getMultiEntry() && $poll->getMultiEntryInterval() == 'without_limit')) {
$response->headers->setCookie($cookie);
}
$response->sendHeaders();
return $response;
}
return $this->render('BaitPollBundle:Poll:poll_results.html.twig', $viewData);
}
public function myPollsAction() {
$this->articleModel = $this->get('eemce.appbundle.article');
$viewData = [];
$viewData['title'] = "Moje súťaže";
$viewData['articles'] = $this->articleModel->getArticlesWithUserParticipationPolls($this->getUser());
return $this->render('BaitPollBundle:Poll:my_polls.html.twig', $viewData);
}
public function pollArchiveAction() {
$this->articleModel = $this->get('eemce.appbundle.article');
$viewData = [];
$viewData['title'] = "Archív súťaží";
$viewData['articles'] = $this->articleModel->getArticlesWithEndedPolls();
return $this->render('BaitPollBundle:Poll:archive.html.twig', $viewData);
}
private function handleError($viewData, $errorMsg, $inputName) {
$viewData["errors"][] = $errorMsg;
$viewData["inputErrors"][] = [
'name' => $inputName,
'error' => $errorMsg,
];
$viewData["alreadyAnswered"] = null;
return $this->render('BaitPollBundle:Poll:poll.html.twig', $viewData);
}
private function getOngoingResults($pollId)
{
/** @var EntityManagerInterface $em */
$em = $this->getDoctrine()->getManager();
/** @var UserAnswer[] $ongoingResult */
$ongoingResult = $em->getRepository('BaitPollBundle:UserAnswer')
->createQueryBuilder('ua')
->join('ua.answer', 'a')
->join('a.question', 'q')
->join('q.poll', 'p')
->where('p.id = :pollId')
->andWhere('q.showOngoingResult = true')
->andWhere('a.type = :radio OR a.type = :dropdown')
->setParameter('pollId', $pollId)
->setParameter('radio', "radio")
->setParameter('dropdown', "dropdown")
->getQuery()
->getResult();
$ongoingResultQuestions = [];
$ongoingResultAnswers = [];
foreach ($ongoingResult as $or) {
$questionId = $or->getQuestion()->getId();
$answerId = $or->getAnswer()->getId();
array_push($ongoingResultQuestions, $questionId);
array_push($ongoingResultAnswers, $answerId);
}
$ongoingResults = [];
$ongoingResults['ongoingResultQuestions'] = array_count_values($ongoingResultQuestions);
$ongoingResults['ongoingResultAnswers'] = array_count_values($ongoingResultAnswers);
// bonus points
/** @var Poll $poll */
$poll = $em->getRepository('BaitPollBundle:Poll')->find($pollId);
/** @var Question $question */
foreach ($poll->getQuestions() as $question) {
if ($question->getShowOngoingResult()) {
/** @var Answer $answer */
foreach ($question->getAnswers() as $answer) {
if ($answer->getBonusPoints() > 0) {
$ongoingResults['ongoingResultQuestions'][$question->getId()] += $answer->getBonusPoints();
$ongoingResults['ongoingResultAnswers'][$answer->getId()] += $answer->getBonusPoints();
}
}
}
}
return $ongoingResults;
}
/**
* Get answer counts for poll
*
* @param Poll $poll
*
* @return int
*/
private function getPollAnswerCounts($poll)
{
/** @var EntityManagerInterface $em */
$em = $this->getDoctrine()->getManager();
$answerGroupCount = $em->getRepository('BaitPollBundle:UserAnswerGroup')
->createQueryBuilder('uag')
->select('COUNT(uag.id)')
->where('uag.poll = :poll')
->setParameter('poll', $poll)
->getQuery()
->getSingleScalarResult();
return $answerGroupCount;
}
private function sendEmail($type, $email)
{
$subject = $this->get('translator')->trans($type);
$message = (new \Swift_Message($subject))
->setFrom($this->getParameter('mailer_sender_email_eshop'), $this->getParameter('mailer_sender_name_eshop'))
->setTo($email)
->setBody(
$this->renderView('BaitPollBundle:Emails:'.$type.'.html.twig'),
'text/html'
)
->addPart(
$this->renderView('BaitPollBundle:Emails:'.$type.'.txt.twig'),
'text/plain'
);
$this->get('mailer')->send($message);
}
/**
* Create slug
*
* @param string $text
*
* @return string
*/
private function createSlug($text)
{
$text = strtolower(strip_tags($text));
$accentedChars = ['À', 'Á', 'Â', 'Ã', 'Ä', 'Å', 'Æ', 'Ç', 'È', 'É', 'Ê', 'Ë', 'Ì', 'Í', 'Î', 'Ï', 'Ð', 'Ñ', 'Ò', 'Ó', 'Ô', 'Õ', 'Ö', 'Ø', 'Ù', 'Ú', 'Û', 'Ü', 'Ý', 'ß', 'à', 'á', 'â', 'ã', 'ä', 'å', 'æ', 'ç', 'è', 'é', 'ê', 'ë', 'ì', 'í', 'î', 'ï', 'ñ', 'ò', 'ó', 'ô', 'õ', 'ö', 'ø', 'ù', 'ú', 'û', 'ü', 'ý', 'ÿ', 'Ā', 'ā', 'Ă', 'ă', 'Ą', 'ą', 'Ć', 'ć', 'Ĉ', 'ĉ', 'Ċ', 'ċ', 'Č', 'č', 'Ď', 'ď', 'Đ', 'đ', 'Ē', 'ē', 'Ĕ', 'ĕ', 'Ė', 'ė', 'Ę', 'ę', 'Ě', 'ě', 'Ĝ', 'ĝ', 'Ğ', 'ğ', 'Ġ', 'ġ', 'Ģ', 'ģ', 'Ĥ', 'ĥ', 'Ħ', 'ħ', 'Ĩ', 'ĩ', 'Ī', 'ī', 'Ĭ', 'ĭ', 'Į', 'į', 'İ', 'ı', 'IJ', 'ij', 'Ĵ', 'ĵ', 'Ķ', 'ķ', 'Ĺ', 'ĺ', 'Ļ', 'ļ', 'Ľ', 'ľ', 'Ŀ', 'ŀ', 'Ł', 'ł', 'Ń', 'ń', 'Ņ', 'ņ', 'Ň', 'ň', 'ʼn', 'Ō', 'ō', 'Ŏ', 'ŏ', 'Ő', 'ő', 'Œ', 'œ', 'Ŕ', 'ŕ', 'Ŗ', 'ŗ', 'Ř', 'ř', 'Ś', 'ś', 'Ŝ', 'ŝ', 'Ş', 'ş', 'Š', 'š', 'Ţ', 'ţ', 'Ť', 'ť', 'Ŧ', 'ŧ', 'Ũ', 'ũ', 'Ū', 'ū', 'Ŭ', 'ŭ', 'Ů', 'ů', 'Ű', 'ű', 'Ų', 'ų', 'Ŵ', 'ŵ', 'Ŷ', 'ŷ', 'Ÿ', 'Ź', 'ź', 'Ż', 'ż', 'Ž', 'ž', 'ſ', 'ƒ', 'Ơ', 'ơ', 'Ư', 'ư', 'Ǎ', 'ǎ', 'Ǐ', 'ǐ', 'Ǒ', 'ǒ', 'Ǔ', 'ǔ', 'Ǖ', 'ǖ', 'Ǘ', 'ǘ', 'Ǚ', 'ǚ', 'Ǜ', 'ǜ', 'Ǻ', 'ǻ', 'Ǽ', 'ǽ', 'Ǿ', 'ǿ'];
$cleanChars = ['A', 'A', 'A', 'A', 'A', 'A', 'AE', 'C', 'E', 'E', 'E', 'E', 'I', 'I', 'I', 'I', 'D', 'N', 'O', 'O', 'O', 'O', 'O', 'O', 'U', 'U', 'U', 'U', 'Y', 's', 'a', 'a', 'a', 'a', 'a', 'a', 'ae', 'c', 'e', 'e', 'e', 'e', 'i', 'i', 'i', 'i', 'n', 'o', 'o', 'o', 'o', 'o', 'o', 'u', 'u', 'u', 'u', 'y', 'y', 'A', 'a', 'A', 'a', 'A', 'a', 'C', 'c', 'C', 'c', 'C', 'c', 'C', 'c', 'D', 'd', 'D', 'd', 'E', 'e', 'E', 'e', 'E', 'e', 'E', 'e', 'E', 'e', 'G', 'g', 'G', 'g', 'G', 'g', 'G', 'g', 'H', 'h', 'H', 'h', 'I', 'i', 'I', 'i', 'I', 'i', 'I', 'i', 'I', 'i', 'IJ', 'ij', 'J', 'j', 'K', 'k', 'L', 'l', 'L', 'l', 'L', 'l', 'L', 'l', 'l', 'l', 'N', 'n', 'N', 'n', 'N', 'n', 'n', 'O', 'o', 'O', 'o', 'O', 'o', 'OE', 'oe', 'R', 'r', 'R', 'r', 'R', 'r', 'S', 's', 'S', 's', 'S', 's', 'S', 's', 'T', 't', 'T', 't', 'T', 't', 'U', 'u', 'U', 'u', 'U', 'u', 'U', 'u', 'U', 'u', 'U', 'u', 'W', 'w', 'Y', 'y', 'Y', 'Z', 'z', 'Z', 'z', 'Z', 'z', 's', 'f', 'O', 'o', 'U', 'u', 'A', 'a', 'I', 'i', 'O', 'o', 'U', 'u', 'U', 'u', 'U', 'u', 'U', 'u', 'U', 'u', 'A', 'a', 'AE', 'ae', 'O', 'o'];
$slug = strtolower(preg_replace(['/[^a-zA-Z0-9 -]/', '/[-]+/', '/^-|-$/'],
['', '-', ''],
str_replace($accentedChars,
$cleanChars,
$text)));
$slug = str_replace(' ',
'-',
$slug);
return $slug;
}
}