/** * Handle saving questions to the database. * @param Integer $quizID The quiz for which the questions apply to. * @param Object $page The associated page object for showing messages. */ function WPCW_showPage_ModifyQuiz_showQuizEntryForms_processSave($quizID, $page) { // No updates have been requested, so exit. if (!isset($_POST['survey_updated'])) { return; } global $wpdb, $wpcwdb; $wpdb->show_errors(); $questionsToSave = array(); $questionsToSave_New = array(); // Check $_POST data for the foreach ($_POST as $key => $value) { // #### 1 - Check if we're deleting a question? // Not interested in new questions that have been added and then deleted. Just the // ones that were added to the database first. if (preg_match('/^delete_wpcw_quiz_details_([0-9]+)$/', $key, $matches)) { $SQL = $wpdb->prepare("\n\t\t\t\tDELETE FROM {$wpcwdb->quiz_qs}\n\t\t\t\tWHERE question_id = %d\n\t\t\t", $matches[1]); $wpdb->query($SQL); } else { if (preg_match('/^question_question_(new_question_)?([0-9]+)$/', $key, $matches)) { // Got the ID of the question, now get answers and correct answer. $questionID = $matches[2]; // Store the extra string if we're adding a new question. $newQuestionPrefix = $matches[1]; $fieldName_Answers = 'question_answer_' . $newQuestionPrefix . $questionID; $fieldName_Correct = 'question_answer_sel_' . $newQuestionPrefix . $questionID; $fieldName_Type = 'question_type_' . $newQuestionPrefix . $questionID; $fieldName_Order = 'question_order_' . $newQuestionPrefix . $questionID; $fieldName_AnswerType = 'question_answer_type_' . $newQuestionPrefix . $questionID; $fieldName_AnswerHint = 'question_answer_hint_' . $newQuestionPrefix . $questionID; // Order should be a number $questionOrder = 0; if (isset($_POST[$fieldName_Order])) { $questionOrder = $_POST[$fieldName_Order] + 0; } // Default types $qAns = false; $qAnsCor = false; $qAnsType = false; // Just used for open question types. $qAnsFileTypes = false; // Just used for upload file types. // Get the hint - Just used for open and upload types. Allow HTML. $qAnsHint = trim(WPCW_arrays_getValue($_POST, $fieldName_AnswerHint)); // What type of question do we have? $questionType = WPCW_arrays_getValue($_POST, $fieldName_Type); switch ($questionType) { case 'multi': $qAns = WPCW_quiz_MultipleChoice::editSave_extractAnswerList($fieldName_Answers); $qAnsCor = WPCW_quiz_MultipleChoice::editSave_extractCorrectAnswer($qAns, $fieldName_Correct); // Provide the UI with at least once slot for an answer. if (!$qAns) { $qAns = array('', ''); } break; case 'open': // See if there's a question type that's been sent back to the server. $answerTypes = WPCW_quiz_OpenEntry::getValidAnswerTypes(); $thisAnswerType = WPCW_arrays_getValue($_POST, $fieldName_AnswerType); // Validate the answer type is in the list. Don't create a default so that user must choose. if (isset($answerTypes[$thisAnswerType])) { $qAnsType = $thisAnswerType; } // There's no correct answer for an open question. $qAnsCor = false; break; case 'upload': $fieldName_FileType = 'question_answer_file_types_' . $newQuestionPrefix . $questionID; // Check new file extension types, parsing them. $qAnsFileTypesRaw = WPCW_files_cleanFileExtensionList(WPCW_arrays_getValue($_POST, $fieldName_FileType)); $qAnsFileTypes = implode(',', $qAnsFileTypesRaw); break; case 'truefalse': $qAnsCor = WPCW_quiz_TrueFalse::editSave_extractCorrectAnswer($fieldName_Correct); break; // Not expecting anything here... so not handling the error case. // Not expecting anything here... so not handling the error case. default: break; } // ### 4 - Save new question data as a list ready for saving to the database. // New question - so no question ID as yet if ($newQuestionPrefix) { $questionsToSave_New[] = array('question_question' => stripslashes($value), 'question_answers' => $qAns ? implode("\n", $qAns) : $qAns, 'question_correct_answer' => $qAnsCor, 'parent_quiz_id' => $quizID, 'question_type' => $questionType, 'question_order' => $questionOrder, 'question_answer_type' => $qAnsType, 'question_answer_hint' => stripslashes($qAnsHint), 'question_answer_file_types' => $qAnsFileTypes); } else { $questionsToSave[$questionID] = array('question_id' => $questionID, 'question_question' => stripslashes($value), 'question_answers' => $qAns ? implode("\n", $qAns) : $qAns, 'question_correct_answer' => $qAnsCor, 'parent_quiz_id' => $quizID, 'question_type' => $questionType, 'question_order' => $questionOrder, 'question_answer_type' => $qAnsType, 'question_answer_hint' => stripslashes($qAnsHint), 'question_answer_file_types' => $qAnsFileTypes); } } } // end if question found. } // #### 5 - Check we have existing questions to save if (count($questionsToSave)) { // Now save all data back to the database. foreach ($questionsToSave as $questionID => $questionDetails) { $wpdb->query(arrayToSQLUpdate($wpcwdb->quiz_qs, $questionDetails, 'question_id')); } } // #### 6 - Save the new questions we have if (count($questionsToSave_New)) { // Now save all data back to the database. foreach ($questionsToSave_New as $questionDetails) { $wpdb->query(arrayToSQLInsert($wpcwdb->quiz_qs, $questionDetails)); } } // Show an error if questions are missing details. $page->showMessage(__('Questions were successfully updated.', 'wp_courseware')); }
/** * Validate the files that have been uploaded, checking them against the conditions of the quiz details. * * @param Array $fileList The list of files to be checked for this quiz. * @param Array $quizDetails The details of the quiz to check * * @return Array The results of the file upload (upload_errors, upload_missing, upload_valid), which contain a list of the question ID and error messages. */ public static function validateFiles($fileList, $quizDetails) { // Assume that we have quiz details at this point. // Get a list of the questions that are expecting files. $questionsWithUploads = array(); foreach ($quizDetails->questions as $qID => $qObj) { if ('upload' == $qObj->question_type) { $questionsWithUploads[$qID] = $qObj; } } // No questions to check for. if (count($questionsWithUploads) == 0) { return false; } // Generate a unique path for the file uploads that uses the user's private directory. $userPathDetails = WPCW_files_getFileUploadDirectory_forUser($quizDetails, get_current_user_id()); // Prepare results data $results = array('upload_errors' => array(), 'upload_missing' => array(), 'upload_valid' => array()); // Check for each expected upload file that's in the list of questions // and do a little more validation (and handle moving the file too). foreach ($questionsWithUploads as $qID => $qObj) { // Generate the name of the file key to check e.g. question_16_upload_73 $keyName = sprintf('question_%d_upload_%d', $quizDetails->quiz_id, $qID); // File was found, so need to some further checks to make sure the extension is valid // and then we can move the file to the right place. if (isset($fileList[$keyName])) { // Uploaded file details $file_name = $fileList[$keyName]['name']; $file_tmp = $fileList[$keyName]['tmp_name']; $file_error = $fileList[$keyName]['error']; $file_size = $fileList[$keyName]['size']; // Got a PHP upload error? if ($file_error > 0) { $errMsg = __('Error. An unknown file upload error occurred.', 'wp_courseware'); switch ($file_error) { case UPLOAD_ERR_FORM_SIZE: case UPLOAD_ERR_INI_SIZE: $errMsg = sprintf(__('Error. The uploaded file exceeds the maximum file upload size (%s).', 'wp_courseware'), WPCW_files_getMaxUploadSize()); break; case UPLOAD_ERR_PARTIAL: $errMsg = __('Error. The uploaded file was only partially uploaded.', 'wp_courseware'); break; case UPLOAD_ERR_NO_FILE: $errMsg = __('Error. No file was uploaded.', 'wp_courseware'); break; case UPLOAD_ERR_NO_TMP_DIR: $errMsg = __('Error. The temporary upload directory does not exist.', 'wp_courseware'); break; case UPLOAD_ERR_CANT_WRITE: $errMsg = __('Error. Could not write the uploaded file to disk.', 'wp_courseware'); break; case UPLOAD_ERR_EXTENSION: $errMsg = __('Error. An extension stopped the file upload.', 'wp_courseware'); break; } // Store error and don't process file further $results['upload_errors'][$qID] = $errMsg; continue; } // Check the valid file extensions $extensionTypes = WPCW_files_cleanFileExtensionList($qObj->question_answer_file_types); $thisFileExtension = pathinfo($file_name, PATHINFO_EXTENSION); // File extension is not valid, so abort and move to next file. if (!in_array($thisFileExtension, $extensionTypes)) { $results['upload_errors'][$qID] = sprintf(__('Error. Extension of file does not match allowed file types of %s.', 'wp_courseware'), implode(', ', $extensionTypes)); continue; } // Move file to the new location, which is USERPATH/question_16_upload_73_user_4.ext so that we can ensure we have @author danielharrison // completely safe URL for the file. And the naming convention helps the admin to a certain degree. $newFilename = $keyName . '_user_' . get_current_user_id() . '.' . $thisFileExtension; if (move_uploaded_file($file_tmp, $userPathDetails['dir_path'] . $newFilename) !== FALSE) { // Store relative path of file as being a valid upload. $results['upload_valid'][$qID] = $userPathDetails['path_only'] . $newFilename; } else { $results['upload_errors'][$qID] = __('Error. Could not move file to your training directory.', 'wp_courseware'); continue; } } else { $results['upload_missing'][$qID] = true; } // end check of question in file list. } // end foreach return $results; }
/** * Handle saving questions to the database. * * @param Integer $quizID The quiz for which the questions apply to. * @param Boolean $singleQuestionMode If true, then we're updating a single question, and we do things slightly differently. */ function WPCW_handler_questions_processSave($quizID, $singleQuestionMode = false) { global $wpdb, $wpcwdb; $wpdb->show_errors(); $questionsToSave = array(); $questionsToSave_New = array(); // Check $_POST data for the foreach ($_POST as $key => $value) { // #### 1 - Check if we're deleting a question from this quiz // We're not just deleting the question, just the association. This is because questions remain in the // pool now. if (preg_match('/^delete_wpcw_quiz_details_([0-9]+)$/', $key, $matches)) { // Remove mapping from the mapping table. $SQL = $wpdb->prepare("\n\t\t\t\tDELETE FROM {$wpcwdb->quiz_qs_mapping}\n\t\t\t\tWHERE question_id = %d\n\t\t\t\t AND parent_quiz_id = %d\n\t\t\t", $matches[1], $quizID); $wpdb->query($SQL); // Update usage counts WPCW_questions_updateUsageCount($matches[1]); // Just a deletion - move on to next array item to save processing time. continue; } // #### 2 - See if we have a question to check for. if (preg_match('/^question_question_(new_question_)?([0-9]+)$/', $key, $matches)) { // Got the ID of the question, now get answers and correct answer. $questionID = $matches[2]; // Store the extra string if we're adding a new question. $newQuestionPrefix = $matches[1]; $fieldName_Answers = 'question_answer_' . $newQuestionPrefix . $questionID; $fieldName_Answers_Img = 'question_answer_image_' . $newQuestionPrefix . $questionID; $fieldName_Correct = 'question_answer_sel_' . $newQuestionPrefix . $questionID; $fieldName_Type = 'question_type_' . $newQuestionPrefix . $questionID; $fieldName_Order = 'question_order_' . $newQuestionPrefix . $questionID; $fieldName_AnswerType = 'question_answer_type_' . $newQuestionPrefix . $questionID; $fieldName_AnswerHint = 'question_answer_hint_' . $newQuestionPrefix . $questionID; $fieldName_Explanation = 'question_answer_explanation_' . $newQuestionPrefix . $questionID; $fieldName_Image = 'question_image_' . $newQuestionPrefix . $questionID; // For Multi-Choice - Answer randomization $fieldName_Multi_Random_Enable = 'question_multi_random_enable_' . $newQuestionPrefix . $questionID; $fieldName_Multi_Random_Count = 'question_multi_random_count_' . $newQuestionPrefix . $questionID; // Order should be a number $questionOrder = 0; if (isset($_POST[$fieldName_Order])) { $questionOrder = $_POST[$fieldName_Order] + 0; } // Default types $qAns = false; $qAnsCor = false; $qAnsType = false; // Just used for open question types. $qAnsFileTypes = false; // Just used for upload file types. // Get the hint - Just used for open and upload types. Allow HTML. $qAnsHint = trim(WPCW_arrays_getValue($_POST, $fieldName_AnswerHint)); // Get the explanation - All questions. Allow HTML. $qAnsExplain = trim(WPCW_arrays_getValue($_POST, $fieldName_Explanation)); // The image URL to use. No HTML. Table record is 300 chars, hence cropping. $qQuesImage = trim(substr(strip_tags(WPCW_arrays_getValue($_POST, $fieldName_Image)), 0, 300)); // How many questions are there is this selection? 1 by default for non-random questions. $expandedQuestionCount = 1; // For Multi-Choice - Answer randomization $qMultiRandomEnable = false; $qMultiRandomCount = 5; // What type of question do we have? $questionType = WPCW_arrays_getValue($_POST, $fieldName_Type); switch ($questionType) { case 'multi': $qAns = WPCW_quiz_MultipleChoice::editSave_extractAnswerList($fieldName_Answers, $fieldName_Answers_Img); $qAnsCor = WPCW_quiz_MultipleChoice::editSave_extractCorrectAnswer($qAns, $fieldName_Correct); // Provide the UI with at least once slot for an answer. if (!$qAns) { $qAns = array('1' => array('answer' => ''), '2' => array('answer' => '')); } // Check randomization values (boolean will be 'on' to enable, as it's a checkbox) $qMultiRandomEnable = 'on' == WPCW_arrays_getValue($_POST, $fieldName_Multi_Random_Enable); $qMultiRandomCount = intval(WPCW_arrays_getValue($_POST, $fieldName_Multi_Random_Count)); break; case 'open': // See if there's a question type that's been sent back to the server. $answerTypes = WPCW_quiz_OpenEntry::getValidAnswerTypes(); $thisAnswerType = WPCW_arrays_getValue($_POST, $fieldName_AnswerType); // Validate the answer type is in the list. Don't create a default so that user must choose. if (isset($answerTypes[$thisAnswerType])) { $qAnsType = $thisAnswerType; } // There's no correct answer for an open question. $qAnsCor = false; break; case 'upload': $fieldName_FileType = 'question_answer_file_types_' . $newQuestionPrefix . $questionID; // Check new file extension types, parsing them. $qAnsFileTypesRaw = WPCW_files_cleanFileExtensionList(WPCW_arrays_getValue($_POST, $fieldName_FileType)); $qAnsFileTypes = implode(',', $qAnsFileTypesRaw); break; case 'truefalse': $qAnsCor = WPCW_quiz_TrueFalse::editSave_extractCorrectAnswer($fieldName_Correct); break; // Validate the the JSON data here... ensure all the tags are valid (not worried about the counts). // Then save back to database. // Validate the the JSON data here... ensure all the tags are valid (not worried about the counts). // Then save back to database. case 'random_selection': // Reset to zero for counting below. $expandedQuestionCount = 0; $decodedTags = WPCW_quiz_RandomSelection::decodeTagSelection(stripslashes($value)); // Capture just ID and count and resave back to database. $toSaveList = false; if (!empty($decodedTags)) { $toSaveList = array(); foreach ($decodedTags as $decodedKey => $decodedDetails) { $toSaveList[$decodedKey] = $decodedDetails['count']; // Track requested questions $expandedQuestionCount += $decodedDetails['count']; } } // Overwrite $value to use cleaned question $value = json_encode($toSaveList); break; // Not expecting anything here... so not handling the error case. // Not expecting anything here... so not handling the error case. default: break; } // ### 4a - Encode the answer data $encodedqAns = $qAns; if (!empty($qAns)) { foreach ($encodedqAns as $idx => $data) { $encodedqAns[$idx]['answer'] = base64_encode($data['answer']); } } // ### 4b - Save new question data as a list ready for saving to the database. $quDataToSave = array('question_answers' => false, 'question_question' => stripslashes($value), 'question_data_answers' => serialize($encodedqAns), 'question_correct_answer' => $qAnsCor, 'question_type' => $questionType, 'question_order' => $questionOrder, 'question_answer_type' => $qAnsType, 'question_answer_hint' => stripslashes($qAnsHint), 'question_answer_explanation' => stripslashes($qAnsExplain), 'question_answer_file_types' => $qAnsFileTypes, 'question_image' => $qQuesImage, 'question_expanded_count' => $expandedQuestionCount, 'question_multi_random_enable' => $qMultiRandomEnable, 'question_multi_random_count' => $qMultiRandomCount, 'taglist' => array()); // ### 5 - Check if there are any tags to save. Only happens for questions that // haven't been saved, so that we can save when we do a $_POST save. $tagFieldForNewQuestions = 'tags_to_add_' . $newQuestionPrefix . $questionID; if (isset($_POST[$tagFieldForNewQuestions])) { if (!empty($_POST[$tagFieldForNewQuestions])) { // Validate each tag ID we have, add to list to be stored for this question later. foreach ($_POST[$tagFieldForNewQuestions] as $idx => $tagText) { $tagText = trim(stripslashes($tagText)); if ($tagText) { $quDataToSave['taglist'][] = $tagText; } } } } // Not a new question - so not got question ID as yet if ($newQuestionPrefix) { $questionsToSave_New[] = $quDataToSave; } else { $quDataToSave['question_id'] = $questionID; $questionsToSave[$questionID] = $quDataToSave; } } // end if question found. } // Only need to adjust quiz settings when editing a quiz and not a single question. if (!$singleQuestionMode) { // #### 6 - Remove association of all questions for this quiz // as we're going to re-add them. $wpdb->query($wpdb->prepare("\n\t\t\t\t\tDELETE FROM {$wpcwdb->quiz_qs_mapping}\n\t\t\t\t\tWHERE parent_quiz_id = %d\n\t\t\t\t", $quizID)); } // #### 7 - Check we have existing questions to save if (count($questionsToSave)) { // Now save all data back to the database. foreach ($questionsToSave as $questionID => $questionDetails) { // Extract the question order, as can't save order with question in DB $questionOrder = $questionDetails['question_order']; unset($questionDetails['question_order']); // Tag list only used for new questions, so remove this field unset($questionDetails['taglist']); // Save question details back to database. $wpdb->query(arrayToSQLUpdate($wpcwdb->quiz_qs, $questionDetails, 'question_id')); // No need to update counts/associations when editing a single lone question if (!$singleQuestionMode) { // Create the association for this quiz/question. $wpdb->query($wpdb->prepare("\n\t\t\t\t\tINSERT INTO {$wpcwdb->quiz_qs_mapping} \n\t\t\t\t\t(question_id, parent_quiz_id, question_order)\n\t\t\t\t\tVALUES (%d, %d, %d)\n\t\t\t\t", $questionID, $quizID, $questionOrder)); // Update usage count for question. WPCW_questions_updateUsageCount($questionID); } } } // #### 8 - Save the new questions we have if (count($questionsToSave_New)) { // Now save all data back to the database. foreach ($questionsToSave_New as $questionDetails) { // Extract the question order, as can't save order with question in DB $questionOrder = $questionDetails['question_order']; unset($questionDetails['question_order']); // Extract the tags added for this question - we'll save manually. $tagsToAddList = $questionDetails['taglist']; unset($questionDetails['taglist']); // Create question in database $wpdb->query(arrayToSQLInsert($wpcwdb->quiz_qs, $questionDetails)); $newQuestionID = $wpdb->insert_id; // No need to update counts/associations when editing a single lone question if (!$singleQuestionMode) { // Create the association for this quiz/question. $wpdb->query($wpdb->prepare("\n\t\t\t\t\tINSERT INTO {$wpcwdb->quiz_qs_mapping} \n\t\t\t\t\t(question_id, parent_quiz_id, question_order)\n\t\t\t\t\tVALUES (%d, %d, %d)\n\t\t\t\t", $newQuestionID, $quizID, $questionOrder)); // Update usage WPCW_questions_updateUsageCount($newQuestionID); } // Add associations for tags for this unsaved question now we finally have a question ID. if (!empty($tagsToAddList)) { WPCW_questions_tags_addTags($newQuestionID, $tagsToAddList); } } } }