Created
January 8, 2020 03:15
-
-
Save meglio/a25f5c9ccc4c038a4f25663920ac23ae to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace Toptal\SpeedCoding\Controller\Api; | |
use Exception; | |
use Respect\Validation\Validator as v; | |
use Toptal\SpeedCoding\Api\ApiResult; | |
use Toptal\SpeedCoding\Auth; | |
use Toptal\SpeedCoding\Challenge\EmailProcessor; | |
use Toptal\SpeedCoding\Challenge\TaskResultComparator; | |
use Toptal\SpeedCoding\Controller\ApiController; | |
use Toptal\SpeedCoding\Db\Expression; | |
use Toptal\SpeedCoding\Form\FormValidator; | |
use Toptal\SpeedCoding\Form\TextWithFormShortcodes; | |
use Toptal\SpeedCoding\Model\ChallengeTable; | |
use Toptal\SpeedCoding\Model\Challenge as ChallengeModel; | |
use Toptal\SpeedCoding\Model\ChallengeTaskTable; | |
use Toptal\SpeedCoding\Model\Entry as EntryModel; | |
use Toptal\SpeedCoding\Model\EntryTable; | |
use Toptal\SpeedCoding\Model\ReferralEmailTable; | |
use Toptal\SpeedCoding\Model\Task as TaskModel; | |
use Toptal\SpeedCoding\Model\TaskAttempt; | |
use Toptal\SpeedCoding\Model\TaskAttemptTable; | |
use Toptal\SpeedCoding\Model\TaskTable; | |
use Toptal\SpeedCoding\Referral\ReferralHash; | |
use Toptal\SpeedCoding\ResponseError\Conflict409Error; | |
use Toptal\SpeedCoding\ResponseError\Forbidden403Error; | |
use Toptal\SpeedCoding\ResponseError\NotFound404Error; | |
use Toptal\SpeedCoding\ResponseError\Redirect; | |
use Toptal\SpeedCoding\ResponseError\UnprocessableEntity422Error; | |
use Toptal\SpeedCoding\Validation\Rules\PasswordsMatch; | |
use Toptal\SpeedCoding\Validation\Validator; | |
class Entry extends ApiController | |
{ | |
/** | |
* Creates a new referral hash for the given challenge and email. | |
* | |
* Used in the Welcome screen. | |
* | |
* Input: | |
* - challengeSlug | |
* | |
* Output: | |
* - refHash | |
* | |
* @return ApiResult | |
* @throws Exception | |
*/ | |
final public function postCreateReferralHashByEmail() | |
{ | |
$di = $this->getDiContainer(); | |
return $this->getDb()->wrapInTransaction(function()use($di){ | |
// Challenge | |
$challenge = $this->getValidateChallengeBySlug(); | |
if (!$challenge->canAcceptANewEntry()) { | |
throw new Forbidden403Error("This challenge does not accept new entries."); | |
} | |
$email = $this->getVar('email'); | |
try { | |
$emailValidator = v::notOptional()->email()->setName('Email'); | |
$emailValidator->check($email); | |
} catch (Exception $e) { | |
throw new UnprocessableEntity422Error("Please provide a valid email address."); | |
} | |
// Create Referral Email record | |
$refEmailTbl = new ReferralEmailTable($di); | |
$refEmail = $refEmailTbl->insertNew($email, $challenge->getId()); | |
// Generate Referral Hash | |
$refHash = (new ReferralHash($di))->generateForReferralEmail( | |
$challenge->getId(), | |
$refEmail->getId() | |
); | |
$result = new ApiResult; | |
$result->setData([ | |
'refHash' => $refHash, | |
]); | |
return $result; | |
}); | |
} | |
/** | |
* Creates a new entry in a challenge. | |
* | |
* Anyone can enter a challenge - no authentication is required. | |
* | |
* Reads referral hash from cookies and applies if found a valid one. | |
* | |
* Input: | |
* - challengeSlug | |
* - leaderboardName | |
* - isConfirmedToBeContacted | |
* | |
* Output: | |
* - entry | |
* - nextTask | |
* - attemptId | |
* | |
* @throws Exception | |
*/ | |
final public function post() | |
{ | |
$db = $this->getDb(); | |
$di = $this->getDiContainer(); | |
return $this->getDb()->wrapInTransaction(function()use($db, $di) { | |
$challenge = $this->getValidateChallengeBySlug(); | |
if (!$challenge->canAcceptANewEntry()) { | |
throw new Forbidden403Error("This challenge does not accept new entries."); | |
} | |
// There must be at least one task available in the challenge | |
$challengeTaskTable = new ChallengeTaskTable($di); | |
$countTasks = $challengeTaskTable->countTasksInChallenge($challenge->getId()); | |
if (!$countTasks) { | |
throw new UnprocessableEntity422Error("Cannot enter a challenge with zero tasks available."); | |
} | |
$email = $this->getVar('email'); | |
try { | |
$emailValidator = v::notOptional()->email()->setName('Email'); | |
$emailValidator->check($email); | |
} catch (Exception $e) { | |
throw new UnprocessableEntity422Error("Please provide a valid email address."); | |
} | |
// Leaderboard Name | |
$leaderboardName = $this->getVar('leaderboardName'); | |
Validator::leaderBoardName(false)->check($leaderboardName); | |
// Retrieve the checkbox value | |
$isConfirmedToBeContacted = $this->getValidateIsConfirmedToBeContacted( | |
$challenge->getConfirmationRequiredText() | |
); | |
// Check if referred by anyone | |
$referredByEmail = null; | |
$ref = ReferralHash::getFromCookies($di); | |
if ($ref && $ref->isValid()) { | |
$referredByEntry = $ref->getEntry(); | |
if ($referredByEntry) { | |
$referredByEmail = $referredByEntry->getEmail(); | |
} | |
if (!$referredByEmail) { | |
$refEmailRecord = $ref->getReferralEmail(); | |
if ($refEmailRecord) { | |
$referredByEmail = $refEmailRecord->getEmail(); | |
} | |
} | |
} | |
// Don't store the referrer email | |
// if it's the same as the entry email | |
if (strtolower($referredByEmail) === strtolower($email)) { | |
$referredByEmail = null; | |
} | |
// Generate a random Entry Key | |
$entryKey = Auth::generateApiKey(); | |
$timeLimitSec = $challenge->getTimeLimitSec(); | |
$modelRow = $db->insertReturning('entry', [ | |
'challenge_id' => $challenge->getId(), | |
'email' => $email, | |
'leaderboard_name' => $leaderboardName, | |
'entry_key' => $entryKey, | |
'total_points' => 0, | |
'date_created' => Expression::currentTimestamp(), | |
'date_stop' => new Expression( | |
"CURRENT_TIMESTAMP + make_interval(secs => $timeLimitSec)" | |
), | |
'confirm_to_be_contacted_text' => $isConfirmedToBeContacted? | |
$challenge->getConfirmToBeContactedText() : '', | |
'is_confirmed_to_be_contacted' => $isConfirmedToBeContacted, | |
'referred_by_email' => $referredByEmail | |
], '*'); | |
$entry = EntryModel::createFromDbRow($modelRow); | |
$entry->setDIContainer($di); | |
// Retrieve the next task, so that we can return it in the same API request | |
$taskMeta = $entry->getNextTaskMeta(); | |
$nextTaskId = $taskMeta->getNextTaskId(); | |
if (!$nextTaskId) { | |
throw new UnprocessableEntity422Error("Cannot enter a challenge with zero tasks available."); | |
} | |
$taskTbl = new TaskTable($di); | |
$task = $taskTbl->fetchById($nextTaskId); | |
if (!$task) { | |
throw new UnprocessableEntity422Error( | |
sprintf( | |
'Could not fetch Next Task by its ID = %s (not found)', | |
$nextTaskId | |
) | |
); | |
} | |
// Create a new task attempt | |
$taskAttemptTbl = new TaskAttemptTable($this->getDiContainer()); | |
$attemptId = $taskAttemptTbl->startNewAttempt($entry->getId(), $task->getId()); | |
$result = new ApiResult; | |
$result->setData([ | |
'entry' => $entry->unsetSensitiveAndAdminOnlyFields(), | |
'nextTask' => $task->unsetSensitiveAndAdminOnlyFields(), | |
'attemptId' => $attemptId | |
]); | |
return $result; | |
}); | |
} | |
/** | |
* Skips a task given the corresponding attempt ID. | |
* | |
* Input: | |
* - attempt_id - the ID of the task to skip (provided in request body) | |
* | |
* Output: | |
* - nextTask - the next task to solve | |
* - attemptId - the ID of the attempt for the next task | |
* | |
* @param int $id The Entry ID (provided in URL) | |
* @throws Exception | |
* | |
* @return ApiResult | |
*/ | |
final public function postSkipTaskById($id) | |
{ | |
$db = $this->getDb(); | |
$di = $this->getDiContainer(); | |
return $this->getDb()->wrapInTransaction(function()use($id, $db, $di) { | |
// Fetch the Entry to be updated | |
/** | |
* Retrieve an Entry by its ID, make sure it has not finished yet. | |
* | |
* @var EntryModel $entry | |
* @var EntryTable $entryTable | |
*/ | |
list($entry, $entryTable) = $this->getValidateEntryById($id, true); | |
$entryId = $entry->getId(); | |
// Fetch the attempt to be skipped | |
/** | |
* @var TaskAttempt $attempt | |
* @var TaskAttemptTable $attemptTbl | |
*/ | |
list($attempt, $attemptTbl) = $this->getValidateAttempt(true); | |
$taskId = $attempt->getTaskId(); | |
// Fetch the task to be skipped and make sure it exists | |
$taskTbl = new TaskTable($this->getDiContainer()); | |
$task = $taskTbl->fetchById($taskId); | |
if (!$task) { | |
throw new UnprocessableEntity422Error("Task ID does not exist."); | |
} | |
// Make sure the task has not been skipped already. | |
// Note. Task can be skipped multiple times, but only as a result | |
// of resetting the list of skipped task IDs. | |
if ($entry->isTaskSkipped($taskId)) { | |
throw new UnprocessableEntity422Error('Cannot skip a task that has already been skipped in this challenge entry.'); | |
} | |
// Disable skipping the last task | |
$entryState = $entry->getState(); | |
if ($entryState->getCountUnsolvedTasks() <= 1) { | |
throw new UnprocessableEntity422Error("Cannot skip last task."); | |
} | |
// Record a skip of the task attempt | |
$attemptTbl->markSkipped($attempt->getId()); | |
// Update the entry with the new skipped task ID | |
$entry->skipTask($taskId); | |
$nextTaskMeta = $entry->getNextTaskMeta(); | |
$nextTaskId = $nextTaskMeta->getNextTaskId(); | |
$nextTask = $taskTbl->fetchById($nextTaskId); | |
if (!$nextTask) { | |
throw new UnprocessableEntity422Error("Could not retrieve the next Task."); | |
} | |
// Create a new task attempt | |
$newAttemptId = $attemptTbl->startNewAttempt($entryId, $nextTaskId); | |
$result = new ApiResult; | |
$result->setData([ | |
'nextTask' => $nextTask->unsetSensitiveAndAdminOnlyFields(), | |
'attemptId' => $newAttemptId | |
]); | |
return $result; | |
}); | |
} | |
/** | |
* Attempts a task. | |
* | |
* Input: | |
* - attempt_id - the ID of the attempt previously created | |
* - code - the user code used to produce tests | |
* - tests_json - a JSON-encoded array of user code test results: | |
* - keys are test nicknames | |
* - values are results returned by the user code | |
* | |
* Output: | |
* - isSuccess - whether all tests passed | |
* - testsFlags - an array of boolean OK/Failed test flags by test nicknames | |
* - isChallengeEntryFinished - whether the challenge entry is finished | |
* because there is no more time / no more tasks to solve | |
* - attemptId - the ID of the next attempt | |
* - nextTask - the next task, if any | |
* - totalPoints - the actual total points value, possibly incremented after | |
* the successful attempt | |
* | |
* @param int $id The Entry ID (provided in URL) | |
* @return ApiResult | |
* @throws Exception | |
*/ | |
final public function postAttemptTaskById($id) | |
{ | |
$db = $this->getDb(); | |
$di = $this->getDiContainer(); | |
return $this->getDb()->wrapInTransaction(function()use($id, $db, $di) { | |
// Fetch the Entry to be updated | |
/** | |
* @var EntryModel $entry | |
* @var EntryTable $entryTbl | |
*/ | |
list($entry, $entryTbl) = $this->getValidateEntryById($id, true); | |
$entryId = $entry->getId(); | |
// Fetch the attempt to be processed | |
/** | |
* @var TaskAttempt $attempt | |
* @var TaskAttemptTable $attemptTbl | |
*/ | |
list($attempt, $attemptTbl) = $this->getValidateAttempt(true); | |
$taskId = $attempt->getTaskId(); | |
// Fetch the task to be attempted and make sure it exists | |
$taskTbl = new TaskTable($this->getDiContainer()); | |
$task = $taskTbl->fetchById($taskId); | |
if (!$task) { | |
throw new UnprocessableEntity422Error("Task ID does not exist."); | |
} | |
// Make sure the task has not been skipped - a skipped task cannot be attempted | |
// until the list of skipped IDs gets full and gets reset. | |
if ($entry->isTaskSkipped($taskId)) { | |
throw new UnprocessableEntity422Error('Cannot process a task attempt that has already been skipped in this challenge entry.'); | |
} | |
// Fetch EntryState for the first time to detect some cases early. | |
// We will re-fetch it again later after updating tables in database. | |
$entryState = $entry->getState(); | |
if ($entryState->isSolvedSuccessfully($taskId)) { | |
throw new UnprocessableEntity422Error("This task has been successfully solved already in this challenge entry and thus cannot be processed again."); | |
} | |
if (!$entryState->isValidTaskId($taskId)) { | |
throw new UnprocessableEntity422Error("This Task is not part of the challenge and thus the attempt cannot be processed."); | |
} | |
// Get user code test results: | |
// - keys are test nicknames | |
// - values are results returned by the user code | |
$tests = $this->getValidateTests(); | |
// The user code for this attempt | |
$code = $this->getValidateCode(); | |
$comp = new TaskResultComparator($task->getTestsJsonDecoded(), $tests); | |
$areAllTestsOK = $comp->areAllTestsOK(); | |
// Finalize the task attempt | |
$attemptTbl->markFinished( | |
$attempt->getId(), | |
$areAllTestsOK, | |
$code, | |
$tests | |
); | |
$apiResData = [ | |
'isSuccess' => $areAllTestsOK, | |
'testsFlags' => $comp->getTests() | |
]; | |
// Attempt SUCCEEDED! | |
if ($areAllTestsOK) { | |
// Increment total points in the entry | |
$plusPoints = $task->getPoints(); | |
$entry->incTotalPoints($plusPoints); | |
// Randomly pick next task ID for tests | |
$nextTaskMeta = $entry->getNextTaskMeta(); | |
$nextTaskId = $nextTaskMeta->getNextTaskId(); | |
} else { | |
// Attempt FAILED | |
// Attempt the same task | |
$nextTaskId = $task->getId(); | |
} | |
$apiResData['totalPoints'] = $entry->getTotalPoints(); | |
if ($entry->isFinished()) { | |
// No more time | |
$isChallengeEntryFinished = true; | |
} else if (!$nextTaskId) { | |
// No more tasks - all tasks completed! | |
$isChallengeEntryFinished = true; | |
$challengeTbl = new ChallengeTable($di); | |
$challenge = $challengeTbl->fetchById($entry->getChallengeId()); | |
if (!$challenge) { | |
throw new UnprocessableEntity422Error("Could not retrieve the Challenge for this Task Attempt."); | |
} | |
$entry->incTotalPointsForRemainingTime($challenge->getPointsPerSecondLeft()); | |
$apiResData['totalPoints'] = $entry->getTotalPoints(); | |
} else { | |
// There are more tasks to tackle! | |
$isChallengeEntryFinished = false; | |
$nextTask = $taskTbl->fetchById($nextTaskId); | |
if (!$nextTask) { | |
throw new UnprocessableEntity422Error("Could not retrieve the next Task."); | |
} | |
$apiResData['nextTask'] = $nextTask->unsetSensitiveAndAdminOnlyFields(); | |
// Create a new task attempt | |
$newAttemptId = $attemptTbl->startNewAttempt($entryId, $nextTaskId); | |
$apiResData['attemptId'] = $newAttemptId; | |
} | |
$apiResData['isChallengeEntryFinished'] = $isChallengeEntryFinished; | |
// When the Entry is finished, process all Challenge Event Emails | |
if ($isChallengeEntryFinished) { | |
$emailProcessor = new EmailProcessor( | |
$this->getDiContainer(), | |
$entry->getChallengeId(), | |
// Passing Entry ID rather then the Entry itself will make | |
// the email processor fetch the latest Entry data from database | |
$entryId | |
); | |
$emailProcessor->process(); | |
} | |
$result = new ApiResult; | |
$result->setData($apiResData); | |
return $result; | |
}); | |
} | |
/** | |
* Submits a form for this entry. | |
* | |
* Forms can be submitted at any time, even after the entries got expired. | |
* | |
* Input: | |
* - entry_key - the Entry Key | |
* - form_response_json - the Form Response array as a JSON-encoded string | |
* | |
* Output: | |
* - (nothing) | |
* | |
* @return ApiResult | |
* @throws Exception | |
*/ | |
final public function postSubmitForm() | |
{ | |
$db = $this->getDb(); | |
$di = $this->getDiContainer(); | |
return $this->getDb()->wrapInTransaction(function()use($db, $di) { | |
/** | |
* @var EntryModel $entry | |
* @var EntryTable $entryTable | |
*/ | |
list($entry, $entryTable) = $this->getValidateEntryByKey(); | |
if ($entry->getIsRemoved()) { | |
return new NotFound404Error('Entry not found'); | |
} | |
// The form has been submitted and recorded already | |
if ($entry->hasFormResponse()) { | |
throw new Conflict409Error('This form has been submitted and recorded already'); | |
} | |
// Validate Form Values form user input | |
$formValues = $this->getValidateFormResponseJson(); | |
// Retrieve the Challenge corresponding to this Entry | |
$challengeTable = $this->newChallengeTable(); | |
/** | |
* @var ChallengeModel $challenge | |
*/ | |
$challenge = $challengeTable->fetchById($entry->getChallengeId()); | |
$html = $challenge->getTyBodyHtml(); | |
$htmlParsed = new TextWithFormShortcodes($di, $html); | |
$form = $htmlParsed->getForm(); | |
if (!$form) { | |
throw new UnprocessableEntity422Error("This challenge does not have any forms."); | |
} | |
$formSpec = $form->getFormSpecJsonDecoded(); | |
$validator = new FormValidator($formSpec, $formValues); | |
if (!$validator->isValid()) { | |
throw new UnprocessableEntity422Error( | |
$validator->getValidationError() | |
); | |
} | |
$validatedValues = $validator->getValidatedValues(); | |
$entry->recordFormSubmission( | |
$formSpec, | |
$validatedValues | |
); | |
return new ApiResult(); | |
}); | |
} | |
/** | |
* @return ChallengeModel | |
* @throws Exception | |
*/ | |
private function getValidateChallengeBySlug() | |
{ | |
// Extract and validate challenge slug | |
$slug = $this->getVar('challengeSlug'); | |
// Slug cannot be empty | |
if (!$slug) { | |
throw new NotFound404Error; | |
} | |
// Check for some sane slug length limit | |
if (mb_strlen($slug) > 1000) { | |
throw new NotFound404Error; | |
} | |
$challengeTable = new ChallengeTable($this->getDiContainer()); | |
$challenge = $challengeTable->fetchBySlug($slug); | |
// The challenge must exist | |
if (!$challenge) { | |
throw new NotFound404Error('Challenge not found.'); | |
} | |
return $challenge; | |
} | |
/** | |
* Retrieves an Entry by its ID, optionally validating it has not finished yet. | |
* @param $id | |
* @param bool $ensureNotFinished | |
* @return array | |
* @throws Exception | |
*/ | |
private function getValidateEntryById($id, $ensureNotFinished = true) | |
{ | |
// Fetch the Entry to be updated | |
$entryTable = new EntryTable($this->getDiContainer()); | |
$entry = $entryTable->fetchById($id); | |
if (!$entry) { | |
throw new NotFound404Error; | |
} | |
if ($ensureNotFinished && $entry->isFinished()) { | |
throw new UnprocessableEntity422Error("This Entry has finished and cannot be updated."); | |
} | |
return [$entry, $entryTable]; | |
} | |
/** | |
* Gets a task by its ID and validates that the task exists. | |
* | |
* @throws Exception | |
*/ | |
private function getValidateTask() | |
{ | |
// Validate input data | |
$fields = $this->getVarsValidate(false, [ | |
'task_id' => [ | |
'is_required' => true, | |
'validator' => function($value) { | |
Validator::id('Task ID')->check($value); | |
} | |
] | |
]); | |
// Make sure the task exists | |
$taskId = $fields['task_id']; | |
$taskTbl = new TaskTable($this->getDiContainer()); | |
$task = $taskTbl->fetchById($taskId); | |
if (!$task) { | |
throw new UnprocessableEntity422Error("Task ID does not exist."); | |
} | |
return [$task, $taskTbl]; | |
} | |
/** | |
* Gets an entry by its Key and validates that the entry exists. | |
* | |
* @return array | |
* @throws Exception | |
*/ | |
private function getValidateEntryByKey() | |
{ | |
$entryKey = $this->getVar('entry_key'); | |
if (!$entryKey || strlen($entryKey) > 1000) { | |
throw new NotFound404Error; | |
} | |
$entryTable = $this->newEntryTable(); | |
$entry = $entryTable->fetchByKey($entryKey); | |
if (!$entry) { | |
throw new NotFound404Error; | |
} | |
return [$entry, $entryTable]; | |
} | |
/** | |
* Gets a Task Attempt by its ID and validates that it exists. | |
* | |
* @param bool $validateIsNotFinished | |
* @return array | |
* @throws Exception | |
*/ | |
private function getValidateAttempt($validateIsNotFinished = true) | |
{ | |
// Validate input data | |
$fields = $this->getVarsValidate(false, [ | |
'attempt_id' => [ | |
'is_required' => true, | |
'validator' => function($value) { | |
Validator::id('Attempt ID')->check($value); | |
} | |
] | |
]); | |
// Make sure the attempt exists | |
$attemptId = $fields['attempt_id']; | |
$attemptTable = new TaskAttemptTable($this->getDiContainer()); | |
$attempt = $attemptTable->fetchById($attemptId); | |
if (!$attempt) { | |
throw new UnprocessableEntity422Error("Attempt ID does not exist."); | |
} | |
if ($validateIsNotFinished && $attempt->isFinished()) { | |
throw new UnprocessableEntity422Error("This attempt was processed already and cannot be processed again."); | |
} | |
return [$attempt, $attemptTable]; | |
} | |
/** | |
* Gets an array of user code test results: | |
* - keys are test nicknames | |
* - values are results returned by user code (usually a scalar value or an array) | |
* | |
* @return array | |
*/ | |
private function getValidateTests() | |
{ | |
$json = $this->getVar('tests_json'); | |
if (!is_string($json) || $json === '') { | |
throw new UnprocessableEntity422Error("Invalid tests JSON."); | |
} | |
// Some sane limits to the tests_json string | |
if (mb_strlen($json) > 100000) { | |
throw new UnprocessableEntity422Error("Tests JSON too long."); | |
} | |
$tests = json_decode($json, true); | |
if (!is_array($tests)) { | |
throw new UnprocessableEntity422Error("Tests JSON is not an array."); | |
} | |
foreach ($tests as $testName => $testResult) { | |
if (!is_string($testName)) { | |
throw new UnprocessableEntity422Error("Test key not a string."); | |
} | |
if (!is_null($testResult) && !is_scalar($testResult) && !is_array($testResult)) { | |
throw new UnprocessableEntity422Error("Test result is not a scalar value and is not an array."); | |
} | |
} | |
return $tests; | |
} | |
private function getValidateCode() | |
{ | |
$code = $this->getVar('code'); | |
if (empty($code)) { | |
$code = ''; | |
} | |
return $code; | |
} | |
/** | |
* Gets and validates the value of the "I confirm to be contacted" checkbox. | |
* | |
* If validation error $confirmationRequiredText is a non-empty string, | |
* then the checkbox is required to be checked; otherwise it is optional. | |
* | |
* @param string $confirmationRequiredText | |
* @return bool | |
*/ | |
private function getValidateIsConfirmedToBeContacted($confirmationRequiredText) | |
{ | |
$isConfirmed = !!$this->getVar('isConfirmedToBeContacted'); | |
// If validation error is a non-empty string, | |
// the checkbox becomes required. | |
if (!$isConfirmed && $confirmationRequiredText !== '') { | |
throw new UnprocessableEntity422Error($confirmationRequiredText); | |
} | |
return $isConfirmed; | |
} | |
/** | |
* Gets an array representing form values from user input: | |
* - keys are field nicknames | |
* - values are the values of the corresponding fields | |
* | |
* @return array | |
*/ | |
private function getValidateFormResponseJson() | |
{ | |
$json = $this->getVar('form_response_json'); | |
if (!is_string($json) || $json === '') { | |
throw new UnprocessableEntity422Error("Invalid Form Response JSON."); | |
} | |
// Some sane limits to the form_response_json string | |
if (mb_strlen('form_response_json') > 100000) { | |
throw new UnprocessableEntity422Error("Form Response JSON too long."); | |
} | |
$formValues = json_decode($json, true); | |
if (!is_array($formValues)) { | |
throw new UnprocessableEntity422Error("Form Response JSON is not an array."); | |
} | |
foreach ($formValues as $fieldKey => $fieldValue) { | |
if (!is_string($fieldKey)) { | |
throw new UnprocessableEntity422Error("Field Key not a string."); | |
} | |
if (!is_scalar($fieldValue) && !is_array($fieldValue)) { | |
throw new UnprocessableEntity422Error("Field Value is not a scalar value and is not an array."); | |
} | |
} | |
return $formValues; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment