diff --git a/.gitignore b/.gitignore index 8ef0e14..6f38042 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ vendor/ .idea/ +.vagrant/ diff --git a/Vagrantfile b/Vagrantfile new file mode 100644 index 0000000..0f3f714 --- /dev/null +++ b/Vagrantfile @@ -0,0 +1,16 @@ +Vagrant.configure("2") do |config| + + ENV["LC_ALL"] = "en_US.UTF-8" + + config.vm.box = "bento/centos-7" + config.vm.hostname = "mariadb.example.local" + + config.vm.synced_folder ".", "/vagrant", disabled: true + + # config.vm.network :bridged + config.vm.network "public_network", bridge: "wlp1s0", ip: "192.168.1.22" + + config.vm.provider "virtualbox" do |v| + v.name = "mariadb" + end +end diff --git a/composer.json b/composer.json index 9e93973..1094150 100644 --- a/composer.json +++ b/composer.json @@ -1,18 +1,19 @@ { - "name": "wikicaptcha/wikicaptcha-backend", - "type": "project", - "require": { - "zendframework/zend-httphandlerrunner": "^1.1", - "zendframework/zend-diactoros": "^2.2", - "psr/http-server-middleware": "^1.0", - "psr/http-message": "^1.0", - "nikic/fast-route": "^1.3", - "relay/relay": "^2.0" - }, - "autoload": - { - "psr-4": { - "Wikicaptcha\\Backend\\": "src/" - } + "name": "wikicaptcha/wikicaptcha-backend", + "type": "project", + "require": { + "zendframework/zend-httphandlerrunner": "^1.1", + "zendframework/zend-diactoros": "^2.2", + "psr/http-server-middleware": "^1.0", + "psr/http-message": "^1.0", + "nikic/fast-route": "^1.3", + "relay/relay": "^2.0", + "ext-pdo": "*", + "ext-json": "*" + }, + "autoload": { + "psr-4": { + "Wikicaptcha\\Backend\\": "src/" } + } } diff --git a/public/index.php b/public/index.php index 9f22a34..f08ea1f 100644 --- a/public/index.php +++ b/public/index.php @@ -1,18 +1,20 @@ handle($request); (new SapiEmitter())->emit($response); \ No newline at end of file diff --git a/src/AnswerDto.php b/src/AnswerDto.php index 4cfbe6a..f08c51e 100644 --- a/src/AnswerDto.php +++ b/src/AnswerDto.php @@ -1,42 +1,63 @@ userAnswer; + } + + /** + * @return string + */ + public function getImgUrl() { + return $this->imgUrl; + } + + /** + * @return string + */ + public function getText() { + return $this->text; + } + private function __consruct() { } public static function fromParts(string $imgUrl, string $text, ?array $userAnswer = null): AnswerDto { $that = new AnswerDto(); $that->imgUrl = $imgUrl; $that->text = $text; if($userAnswer !== null) { $that->userAnswer = UserAnswerDto::fromArray($userAnswer); } return $that; } public static function fromArray(array $array): AnswerDto { if(isset($array['userAnswer'])) { return self::fromParts($array['imgUrl'], $array['text'], $array['userAnswer']); } else { return self::fromParts($array['imgUrl'], $array['text']); } } public function jsonSerialize() { $result = []; if($this->userAnswer !== null) { $result['userAnswer'] = $this->userAnswer; } $result['imgUrl'] = $this->imgUrl; $result['text'] = $this->text; return $result; } } diff --git a/src/Answers.php b/src/Answers.php index 4277133..72f2788 100644 --- a/src/Answers.php +++ b/src/Answers.php @@ -1,21 +1,97 @@ getAttribute('ParsedBody', []); - $questions = WikiApiDto::fromArray($payload); + $pdo = (new Database())->getPdo(); + $thing = WikiApiDto::fromArray($payload); + $sessionId = $thing->getSessionId(); - // TODO: check that user is a human or a bot + $correct = 0; + $wrong = 0; - return new JsonResponse($questions); + foreach($thing->getQuestionList() as $question) { + $question->getQuestionId(); + + $connotationsStmt = $pdo->prepare('SELECT connotation_ID FROM wcaptcha_challenge_connotation WHERE challenge_ID = ?'); + $connotationsStmt->execute([$sessionId]); + + $questionConnotations = []; + foreach($connotationsStmt->fetchAll() as $connotation) { + $questionConnotations[] = $connotation['connotation_ID']; + } + + foreach($question->getAnswersAvailable() as $answer) { + $img = $answer->getImgUrl(); + $imgId = (int) $answer->getText(); + $answer = $answer->getUserAnswer(); + $selected = isset($answer->selected) ?? null; + unset($answer); + + if($selected === null) { + return new JsonResponse(['human' => false, 'why' => 'Select or not select every image']); + } + $connotationsStmt = $pdo->prepare('SELECT image_ID, connotation_ID, image_connotation_positive FROM wcaptcha_image_connotation WHERE image_ID = ?'); + $connotationsStmt->execute([$imgId]); + foreach($connotationsStmt->fetchAll() as $connotation) { + if($connotation['image_connotation_positive'] === 'positive') { + // Any of these selected connotations is right + if($selected) { + // Has been selected: connotation selected by user is in array of image connotations + if(in_array($connotation['connotation_ID'], $questionConnotations)) { + // Users says so + $correct++; + } else { + $wrong++; + } + } else { + // Not selected: connotation in image has not been selected but it was one in the question + if(in_array($connotation['connotation_ID'], $questionConnotations)) { + $wrong++; + } else { + $correct++; + } + } + } else { + // Any of these selected connotations is wrong + if($selected) { + if(in_array($connotation['connotation_ID'], $questionConnotations)) { + $wrong++; + } else { + $correct++; + } + } else { + if(in_array($connotation['connotation_ID'], $questionConnotations)) { + $correct++; + } else { + $wrong++; + } + } + } + } + + $pdo->beginTransaction(); + $pdo->commit(); + + } + } + + //return new JsonResponse($questions); + + // A real system would probably not give this information to the user, but to the server + if($correct < $wrong) { + return new JsonResponse(['human' => true, 'correct' => $correct, 'wrong' => $wrong]); + } else { + return new JsonResponse(['human' => false, 'correct' => $correct, 'wrong' => $wrong]); + } } -} \ No newline at end of file +} diff --git a/src/Database.php b/src/Database.php new file mode 100644 index 0000000..410b475 --- /dev/null +++ b/src/Database.php @@ -0,0 +1,21 @@ + \PDO::ERRMODE_EXCEPTION, + \PDO::ATTR_DEFAULT_FETCH_MODE => \PDO::FETCH_ASSOC, + \PDO::ATTR_EMULATE_PREPARES => false, + ]; + $this->pdo = new \PDO('mysql:host=192.168.69.1;dbname=wikicaptcha', 'wikicaptcha', 'ahvahl8MefoaPh2vui8aewaNgiehei', $options); + } + + public function getPdo() { + return $this->pdo; + } +} diff --git a/src/QuestionDto.php b/src/QuestionDto.php index 1f4e4ba..22f196c 100644 --- a/src/QuestionDto.php +++ b/src/QuestionDto.php @@ -1,66 +1,80 @@ 'IMG', 'INPUT' => 'INPUT', // free text 'OPTIONS' => 'OPTIONS' ]; + /** + * @return AnswerDto[] + */ + public function getAnswersAvailable() { + return $this->answersAvailable; + } + + /** + * @return mixed + */ + public function getQuestionId() { + return $this->questionId; + } + private function __consruct() { } protected $questionText; protected $questionId; protected $questionType; protected $answersAvailable; public static function fromParts(string $id, string $text, string $type, array $answersAvailable): QuestionDto { $that = new QuestionDto(); $that->questionId = $id; $that->questionText = $text; $that->setQuestionType($type); $that->setAnswersAvailable($answersAvailable); return $that; } protected function setQuestionType(string $questionType) { if(!isset(self::QUESTION_TYPES[$questionType])) { throw new InvalidArgumentException("$questionType is not a valid question type"); } $this->questionType = $questionType; } protected function setAnswersAvailable(array $answersAvailable) { foreach($answersAvailable as $item) { if(!($item instanceof AnswerDto)) { throw new InvalidArgumentException("answersAvailable must be instanceof AnswerDto"); } } $this->answersAvailable = $answersAvailable; } public static function fromArray(array $array): QuestionDto { $a = []; foreach($array['answersAvailable'] as $el) { $a[] = AnswerDto::fromArray($el); } return self::fromParts($array['questionId'], $array['questionText'], $array['questionType'], $a); } public function jsonSerialize() { $result = []; $result['questionText'] = $this->questionText; $result['questionId'] = $this->questionId; $result['questionType'] = $this->questionType; $result['answersAvailable'] = $this->answersAvailable; return $result; } } diff --git a/src/Questions.php b/src/Questions.php index 52be4ad..4f91c45 100644 --- a/src/Questions.php +++ b/src/Questions.php @@ -1,39 +1,114 @@ getAttribute('ParsedBody', []); if(!isset($payload['language'])) { return new JsonResponse(['error' => 'Add language to request'], 400); } + if(!isset($payload['appid'])) { + return new JsonResponse(['error' => 'Add appid to request'], 400); + } + + $pdo = (new Database())->getPdo(); + $pdo->beginTransaction(); + + $ip = inet_pton($_SERVER['REMOTE_ADDR']); + $s = $pdo->prepare('INSERT INTO wcaptcha_session(session_ip, app_id) VALUES (?, ?)'); + $s->execute([$ip, (int) $payload['appid']]); + $id = $pdo->lastInsertId(); + + $pdo->commit(); - // TODO: query database - $id = 123; $questions = []; - for($i = 0; $i < 3; $i++) { + $challengeRows = $pdo->query('SELECT challenge_ID, challenge_type, challenge_text FROM wcaptcha_challenge ORDER BY RAND() LIMIT 2'); + $total = 9; + $actual = mt_rand(2, 8); + foreach($challengeRows as $challenge) { + switch($challenge['challenge_type']) { + case 'img': + $type = QuestionDto::QUESTION_TYPES['IMG']; + break; + case 'text': + default: + $type = QuestionDto::QUESTION_TYPES['INPUT']; + break; + case 'option': + $type = QuestionDto::QUESTION_TYPES['OPTIONS']; + break; + } + $answers = []; - $answers[0] = AnswerDto::fromParts('https://upload.wikimedia.org/wikipedia/commons/thumb/d/d4/George-W-Bush.jpeg/440px-George-W-Bush.jpeg', 'This is some text'); - $answers[1] = AnswerDto::fromParts('https://upload.wikimedia.org/wikipedia/commons/thumb/d/d4/George-W-Bush.jpeg/440px-George-W-Bush.jpeg', 'This is some text'); - $answers[2] = AnswerDto::fromParts('https://upload.wikimedia.org/wikipedia/commons/thumb/d/d4/George-W-Bush.jpeg/440px-George-W-Bush.jpeg', 'This is some text'); - $answers[3] = AnswerDto::fromParts('https://upload.wikimedia.org/wikipedia/commons/thumb/8/8d/President_Barack_Obama.jpg/440px-President_Barack_Obama.jpg', 'This is some text'); - $answers[4] = AnswerDto::fromParts('https://upload.wikimedia.org/wikipedia/commons/thumb/5/56/Donald_Trump_official_portrait.jpg/440px-Donald_Trump_official_portrait.jpg', 'This is some text'); - $answers[5] = AnswerDto::fromParts('https://upload.wikimedia.org/wikipedia/commons/thumb/5/56/Donald_Trump_official_portrait.jpg/440px-Donald_Trump_official_portrait.jpg', 'This is some text'); - $answers[6] = AnswerDto::fromParts('https://upload.wikimedia.org/wikipedia/commons/thumb/5/56/Donald_Trump_official_portrait.jpg/440px-Donald_Trump_official_portrait.jpg', 'This is some text'); - $answers[7] = AnswerDto::fromParts('https://upload.wikimedia.org/wikipedia/commons/thumb/5/56/Donald_Trump_official_portrait.jpg/440px-Donald_Trump_official_portrait.jpg', 'This is some text'); - $answers[8] = AnswerDto::fromParts('https://upload.wikimedia.org/wikipedia/commons/thumb/5/56/Donald_Trump_official_portrait.jpg/440px-Donald_Trump_official_portrait.jpg', 'This is some text'); - $questions[$i] = QuestionDto::fromParts('q' . mt_rand(100, 999), 'Which one of these persons is Barack Obama?', QuestionDto::QUESTION_TYPES['IMG'], $answers); + $already = []; + + if($type === QuestionDto::QUESTION_TYPES['IMG']) { + + // Actual images + + $imageRows = $pdo->prepare(' +SELECT image_ID, image_src +FROM wcaptcha_challenge_connotation +NATURAL JOIN wcaptcha_image_connotation +NATURAL JOIN wcaptcha_image +WHERE wcaptcha_challenge_connotation.challenge_ID = ? +GROUP BY image_ID, image_src +ORDER BY COUNT(*) DESC +LIMIT ? +'); + $imageRows->execute([$challenge['challenge_ID'], $actual]); + foreach($imageRows->fetchAll() as $image) { + // $filename = array_reverse(explode('/', $image['image_src']))[0]; + // $answers[] = AnswerDto::fromParts($image['image_src'] . '/' . "/200px-$filename", (string) $image['image_ID']); + $answers[] = AnswerDto::fromParts($image['image_src'], (string) $image['image_ID']); + $already[] = $image['image_ID']; + } + + $actual = count($already); + $random = $total - $actual; + + if($actual <= 0) { + throw new \LogicException('No answers for challenge ' . $challenge['challenge_ID']); + } + + // Random images + + $questionMarks = []; + foreach($already as $ignored) { + $questionMarks[] = '?'; + } + unset($ignored); + + $stmt = ' +SELECT image_ID, image_src +FROM wcaptcha_image +WHERE image_ID NOT IN (' . implode(', ', $questionMarks) .') +ORDER BY RAND() +LIMIT ?'; + $params = array_merge($already, [$random]); + + $imageRows2 = $pdo->prepare($stmt); + $imageRows2->execute($params); + foreach($imageRows2->fetchAll() as $image) { + $answers[] = AnswerDto::fromParts($image['image_src'], 'No text for you'); + } + } else { + throw new \LogicException('Not implemented'); + } + + shuffle($answers); + $questions[] = QuestionDto::fromParts($challenge['challenge_ID'], $challenge['challenge_text'], $type, $answers); } return new JsonResponse(WikiApiDto::fromParts($id, $questions), 200); } } diff --git a/src/WikiApiDto.php b/src/WikiApiDto.php index 37c5f2a..d26fde4 100644 --- a/src/WikiApiDto.php +++ b/src/WikiApiDto.php @@ -1,31 +1,45 @@ sessionId = $sessionId; $that->questionList = $questionList; return $that; } public function jsonSerialize() { $result = []; $result['sessionId'] = $this->sessionId; $result['questionList'] = $this->questionList; return $result; } + + /** + * @return QuestionDto[] + */ + public function getQuestionList() { + return $this->questionList; + } + + /** + * @return int + */ + public function getSessionId() { + return $this->sessionId; + } }