diff --git a/application/helpers/remotecontrol/remotecontrol_handle.php b/application/helpers/remotecontrol/remotecontrol_handle.php index 3e621755cbe..b6fa9d9169f 100644 --- a/application/helpers/remotecontrol/remotecontrol_handle.php +++ b/application/helpers/remotecontrol/remotecontrol_handle.php @@ -1471,102 +1471,136 @@ public function delete_question($sSessionKey, $iQuestionID) public function import_question($sSessionKey, $iSurveyID, $iGroupID, $sImportData, $sImportDataType, $sMandatory = 'N', $sNewQuestionTitle = null, $sNewqQuestion = null, $sNewQuestionHelp = null) { $bOldEntityLoaderState = null; - if ($this->_checkSessionKey($sSessionKey)) { - $iSurveyID = (int) $iSurveyID; - $iGroupID = (int) $iGroupID; - $oSurvey = Survey::model()->findByPk($iSurveyID); - if (!isset($oSurvey)) { - return array('status' => 'Error: Invalid survey ID'); - } - - if (Permission::model()->hasSurveyPermission($iSurveyID, 'survey', 'update')) { - if ($oSurvey->isActive) { - return array('status' => 'Error:Survey is Active and not editable'); - } + if (!$this->_checkSessionKey($sSessionKey)) { + return array('status' => self::INVALID_SESSION_KEY); + } + $iSurveyID = (int) $iSurveyID; + $iGroupID = (int) $iGroupID; + $oSurvey = Survey::model()->findByPk($iSurveyID); + if (!isset($oSurvey)) { + return array('status' => 'Error: Invalid survey ID'); + } + if (!Permission::model()->hasSurveyPermission($iSurveyID, 'surveycontent', 'update') && !Permission::model()->hasSurveyPermission($iSurveyID, 'surveycontent', 'import')) { + return array('status' => 'No permission'); + } + if ($oSurvey->isActive) { + return array('status' => 'Error:Survey is Active and not editable'); + } - $oGroup = QuestionGroup::model()->findByAttributes(array('gid' => $iGroupID)); - if (!isset($oGroup)) { - return array('status' => 'Error: Invalid group ID'); - } + $oGroup = QuestionGroup::model()->findByAttributes(array('gid' => $iGroupID)); + if (!isset($oGroup)) { + return array('status' => 'Error: Invalid group ID'); + } - $sGroupSurveyID = $oGroup['sid']; - if ($sGroupSurveyID != $iSurveyID) { - return array('status' => 'Error: Missmatch in surveyid and groupid'); - } + $sGroupSurveyID = $oGroup['sid']; + if ($sGroupSurveyID != $iSurveyID) { + return array('status' => 'Error: Missmatch in surveyid and groupid'); + } + /* Check unicity of title, and set autorename to true if it's set */ + $importOptions = ['autorename' => false]; + if (!empty($sNewQuestionTitle)) { + $countQuestionTitle = intval(Question::model()->count( + "sid = :sid and parent_qid = 0 and title = :title", + array( + ":sid" => $iSurveyID, + ":title" => $sNewQuestionTitle + ) + )); + if ($countQuestionTitle > 0) { + return array('status' => 'Error: Question title already exist in this survey.'); + } + /* This allow import with existing title */ + $importOptions = ['autorename' => true]; + } + if (!strtolower($sImportDataType) == 'lsq') { + return array('status' => 'Invalid extension'); + } - if (!strtolower($sImportDataType) == 'lsq') { - return array('status' => 'Invalid extension'); - } - libxml_use_internal_errors(true); - Yii::app()->loadHelper('admin/import'); - // First save the data to a temporary file - $sFullFilePath = Yii::app()->getConfig('tempdir') . DIRECTORY_SEPARATOR . randomChars(40) . '.' . $sImportDataType; - file_put_contents($sFullFilePath, base64_decode(chunk_split($sImportData))); + libxml_use_internal_errors(true); + Yii::app()->loadHelper('admin.import'); - if (strtolower($sImportDataType) == 'lsq') { - if (\PHP_VERSION_ID < 80000) { - $bOldEntityLoaderState = libxml_disable_entity_loader(true); // @see: http://phpsecurity.readthedocs.io/en/latest/Injection-Attacks.html#xml-external-entity-injection - } - $sXMLdata = file_get_contents($sFullFilePath); - $xml = @simplexml_load_string($sXMLdata, 'SimpleXMLElement', LIBXML_NONET); - if (!$xml) { - unlink($sFullFilePath); - if (\PHP_VERSION_ID < 80000) { - libxml_disable_entity_loader($bOldEntityLoaderState); // Put back entity loader to its original state, to avoid contagion to other applications on the server - } - return array('status' => 'Error: Invalid LimeSurvey question structure XML '); - } - $aImportResults = XMLImportQuestion($sFullFilePath, $iSurveyID, $iGroupID); - } else { - if (\PHP_VERSION_ID < 80000) { - libxml_disable_entity_loader($bOldEntityLoaderState); // Put back entity loader to its original state, to avoid contagion to other applications on the server - } - return array('status' => 'Really Invalid extension'); //just for symmetry! - } + // First save the data to a temporary file + $sFullFilePath = App()->getConfig('tempdir') . DIRECTORY_SEPARATOR . randomChars(40) . '.' . $sImportDataType; + file_put_contents($sFullFilePath, base64_decode(chunk_split($sImportData))); + if (strtolower($sImportDataType) == 'lsq') { + if (\PHP_VERSION_ID < 80000) { + $bOldEntityLoaderState = libxml_disable_entity_loader(true); // @see: http://phpsecurity.readthedocs.io/en/latest/Injection-Attacks.html#xml-external-entity-injection + } + $sXMLdata = file_get_contents($sFullFilePath); + $xml = @simplexml_load_string($sXMLdata, 'SimpleXMLElement', LIBXML_NONET); + if (!$xml) { unlink($sFullFilePath); + if (\PHP_VERSION_ID < 80000) { + libxml_disable_entity_loader($bOldEntityLoaderState); // Put back entity loader to its original state, to avoid contagion to other applications on the server + } + return array('status' => 'Error: Invalid LimeSurvey question structure XML '); + } + $aImportResults = XMLImportQuestion($sFullFilePath, $iSurveyID, $iGroupID, $importOptions); + } else { + if (\PHP_VERSION_ID < 80000) { + libxml_disable_entity_loader($bOldEntityLoaderState); // Put back entity loader to its original state, to avoid contagion to other applications on the server + } + return array('status' => 'Really Invalid extension'); //just for symmetry! + } + unlink($sFullFilePath); + $iNewqid = 0; + if (isset($aImportResults['fatalerror'])) { + if (\PHP_VERSION_ID < 80000) { + libxml_disable_entity_loader($bOldEntityLoaderState); // Put back entity loader to its original state, to avoid contagion to other applications on the server + } + return array('status' => 'Error: ' . $aImportResults['fatalerror']); + } else { + if (\PHP_VERSION_ID < 80000) { + libxml_disable_entity_loader($bOldEntityLoaderState); // Put back entity loader to its original state, to avoid contagion to other applications on the server + } + fixLanguageConsistency($iSurveyID); - if (isset($aImportResults['fatalerror'])) { - if (\PHP_VERSION_ID < 80000) { - libxml_disable_entity_loader($bOldEntityLoaderState); // Put back entity loader to its original state, to avoid contagion to other applications on the server - } - return array('status' => 'Error: ' . $aImportResults['fatalerror']); - } else { - fixLanguageConsistency($iSurveyID); - $iNewqid = $aImportResults['newqid']; + $iNewqid = $aImportResults['newqid']; + /* @var array[] validation errors */ + $errors = []; + $oQuestion = Question::model()->findByAttributes(array('sid' => $iSurveyID, 'gid' => $iGroupID, 'qid' => $iNewqid)); + if (in_array($sMandatory, array('Y', 'S', 'N'))) { + $oQuestion->setAttribute('mandatory', $sMandatory); + } else { + $oQuestion->setAttribute('mandatory', 'N'); + } + if (!empty($sNewQuestionTitle)) { + $oQuestion->setAttribute('title', $sNewQuestionTitle); + } - $oQuestion = Question::model()->findByAttributes(array('sid' => $iSurveyID, 'gid' => $iGroupID, 'qid' => $iNewqid)); - if ($sNewQuestionTitle != null) { - $oQuestion->setAttribute('title', $sNewQuestionTitle); - } - if ($sNewqQuestion != '') { - $oQuestion->setAttribute('question', $sNewqQuestion); - } - if ($sNewQuestionHelp != '') { - $oQuestion->setAttribute('help', $sNewQuestionHelp); - } - if (in_array($sMandatory, array('Y', 'S', 'N'))) { - $oQuestion->setAttribute('mandatory', $sMandatory); - } else { - $oQuestion->setAttribute('mandatory', 'N'); - } + if (!$oQuestion->save()) { + return array( + 'status' => 'Error when update question', + 'errors' => $oQuestion->getErrors() + ); + } - if (\PHP_VERSION_ID < 80000) { - libxml_disable_entity_loader($bOldEntityLoaderState); // Put back entity loader to its original state, to avoid contagion to other applications on the server - } + $oQuestionL10ns = QuestionL10n::model()->findAll( + "qid = :qid", + array(':qid' => $iNewqid) + ); - try { - $oQuestion->save(); - } catch (Exception $e) { - // no need to throw exception - } - return (int) $aImportResults['newqid']; + foreach ($oQuestionL10ns as $oQuestionL10n) { + if (!empty($sNewqQuestion)) { + $oQuestionL10n->setAttribute('question', $sNewqQuestion); + } + if (!empty($sNewQuestionHelp)) { + $oQuestionL10n->setAttribute('help', $sNewQuestionHelp); + } + if (!$oQuestionL10n->save()) { + $errors[] = $oQuestionL10n->getErrors(); } - } else { - return array('status' => 'No permission'); } - } else { - return array('status' => self::INVALID_SESSION_KEY); + + if (!empty($errors)) { + return array( + 'status' => 'Error when update question', + 'errors' => $errors + ); + } + + return intval($iNewqid); } } @@ -2182,7 +2216,7 @@ public function list_participants($sSessionKey, $iSurveyID, $iStart = 0, $iLimit $aAttributeValues = array(); if (count($aConditions) > 0) { $aConditionFields = array_flip(Token::model($iSurveyID)->getMetaData()->tableSchema->columnNames); - // NB: $valueOrTuple is either a value or tuple like [$operator, $value]. + // NB: $valueOrTuple is either a value or tuple like [$operator, $value]. foreach ($aConditions as $columnName => $valueOrTuple) { if (is_array($valueOrTuple)) { /** @var string[] List of operators allowed in query. */ diff --git a/tests/data/surveys/limesurvey_question_import_question_test.lsq b/tests/data/surveys/limesurvey_question_import_question_test.lsq new file mode 100644 index 00000000000..b50e21ccf30 --- /dev/null +++ b/tests/data/surveys/limesurvey_question_import_question_test.lsq @@ -0,0 +1,125 @@ + + + Question + 366 + + en + + + + qid + parent_qid + sid + gid + type + title + question + preg + help + other + mandatory + question_order + language + scale_id + same_default + relevance + modulename + + + + + + + + + <![CDATA[Q00]]> + + + + + + + + + + + + + + + + qid + attribute + value + language + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/data/surveys/limesurvey_question_import_question_test_II.lsq b/tests/data/surveys/limesurvey_question_import_question_test_II.lsq new file mode 100644 index 00000000000..7ee925e03f5 --- /dev/null +++ b/tests/data/surveys/limesurvey_question_import_question_test_II.lsq @@ -0,0 +1,49 @@ + + + Question + 366 + + en + + + + qid + parent_qid + sid + gid + type + title + question + preg + help + other + mandatory + question_order + language + scale_id + same_default + relevance + modulename + + + + + + + + + <![CDATA[G01Q02]]> + + + + + + + + + + + + + + diff --git a/tests/data/surveys/limesurvey_survey_import_question_test.lss b/tests/data/surveys/limesurvey_survey_import_question_test.lss new file mode 100644 index 00000000000..52094af61c6 --- /dev/null +++ b/tests/data/surveys/limesurvey_survey_import_question_test.lss @@ -0,0 +1,336 @@ + + + Survey + 366 + + en + + + + gid + sid + group_name + group_order + description + language + randomization_group + grelevance + + + + + + + + + + + + + + + + + qid + parent_qid + sid + gid + type + title + question + preg + help + other + mandatory + question_order + language + scale_id + same_default + relevance + modulename + + + + + + + + + <![CDATA[Q00]]> + + + + + + + + + + + + + + + + qid + attribute + value + language + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + sid + gsid + admin + expires + startdate + adminemail + anonymized + faxto + format + savetimings + template + language + additional_languages + datestamp + usecookie + allowregister + allowsave + autonumber_start + autoredirect + allowprev + printanswers + ipaddr + refurl + showsurveypolicynotice + publicstatistics + publicgraphs + listpublic + htmlemail + sendconfirmation + tokenanswerspersistence + assessments + usecaptcha + usetokens + bounce_email + attributedescriptions + emailresponseto + emailnotificationto + tokenlength + showxquestions + showgroupinfo + shownoanswer + showqnumcode + bouncetime + bounceprocessing + bounceaccounttype + bounceaccounthost + bounceaccountpass + bounceaccountencryption + bounceaccountuser + showwelcome + showprogress + questionindex + navigationdelay + nokeyboard + alloweditaftercompletion + googleanalyticsstyle + googleanalyticsapikey + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + surveyls_survey_id + surveyls_language + surveyls_title + surveyls_description + surveyls_welcometext + surveyls_endtext + surveyls_policy_notice + surveyls_policy_error + surveyls_policy_notice_label + surveyls_url + surveyls_urldescription + surveyls_email_invite_subj + surveyls_email_invite + surveyls_email_remind_subj + surveyls_email_remind + surveyls_email_register_subj + surveyls_email_register + surveyls_email_confirm_subj + surveyls_email_confirm + surveyls_dateformat + surveyls_attributecaptions + email_admin_notification_subj + email_admin_notification + email_admin_responses_subj + email_admin_responses + surveyls_numberformat + attachments + + + + + + + + +
You have been invited to participate in a survey.

The survey is titled:
"{SURVEYNAME}"

"{SURVEYDESCRIPTION}"

To participate, please click on the link below.

Sincerely,

{ADMINNAME} ({ADMINEMAIL})

----------------------------------------------
Click here to do the survey:
{SURVEYURL}

If you do not want to participate in this survey and don't want to receive any more invitations please click the following link:
{OPTOUTURL}

If you are blacklisted but want to participate in this survey and want to receive invitations please click the following link:
{OPTINURL}]]>
+ +
Recently we invited you to participate in a survey.

We note that you have not yet completed the survey, and wish to remind you that the survey is still available should you wish to take part.

The survey is titled:
"{SURVEYNAME}"

"{SURVEYDESCRIPTION}"

To participate, please click on the link below.

Sincerely,

{ADMINNAME} ({ADMINEMAIL})

----------------------------------------------
Click here to do the survey:
{SURVEYURL}

If you do not want to participate in this survey and don't want to receive any more invitations please click the following link:
{OPTOUTURL}]]>
+ +
You, or someone using your email address, have registered to participate in an online survey titled {SURVEYNAME}.

To complete this survey, click on the following URL:

{SURVEYURL}

If you have any questions about this survey, or if you did not register to participate and believe this email is in error, please contact {ADMINNAME} at {ADMINEMAIL}.]]>
+ +
This email is to confirm that you have completed the survey titled {SURVEYNAME} and your response has been saved. Thank you for participating.

If you have any further questions about this email, please contact {ADMINNAME} on {ADMINEMAIL}.

Sincerely,

{ADMINNAME}]]>
+ + +
A new response was submitted for your survey '{SURVEYNAME}'.

Click the following link to see the individual response:
{VIEWRESPONSEURL}

Click the following link to edit the individual response:
{EDITRESPONSEURL}

View statistics by clicking here:
{STATISTICSURL}]]>
+ +
A new response was submitted for your survey '{SURVEYNAME}'.

Click the following link to see the individual response:
{VIEWRESPONSEURL}

Click the following link to edit the individual response:
{EDITRESPONSEURL}

View statistics by clicking here:
{STATISTICSURL}


The following answers were given by the participant:
{ANSWERTABLE}]]>
+ +
+
+
+ + + 124268 + vanilla + + inherit + + + + + + 124268 + vanilla + + + off + on + on + off + ./files/logo.png + noto + + + + +
diff --git a/tests/unit/helpers/remotecontrol/RemoteControlImportQuestionTest.php b/tests/unit/helpers/remotecontrol/RemoteControlImportQuestionTest.php new file mode 100644 index 00000000000..0593fe2d4d7 --- /dev/null +++ b/tests/unit/helpers/remotecontrol/RemoteControlImportQuestionTest.php @@ -0,0 +1,136 @@ +handler->get_session_key($this->getUsername(), $this->getPassword()); + + /** @var integer the only group id */ + $testGroupId = self::$testSurvey->groups[0]->gid; + + // Attempt Importing Question + $questionFile = self::$surveysFolder . '/limesurvey_question_import_question_test_II.lsq'; + $question = base64_encode(file_get_contents($questionFile)); + $result = $this->handler->import_question($sessionKey, self::$surveyId, $testGroupId, $question, 'lsq'); + $this->assertIsInt($result, 'There was an error importing a question with a code that did not already exists.'); + } + + /** + * Importing a question with a question code that already exists. + */ + public function testImportQuestionWithRepeatedQuestionCode() + { + $sessionKey = $this->handler->get_session_key($this->getUsername(), $this->getPassword()); + + /** @var integer the only group id */ + $testGroupId = self::$testSurvey->groups[0]->gid; + + // Attempt Importing Question + $questionFile = self::$surveysFolder . '/limesurvey_question_import_question_test.lsq'; + $question = base64_encode(file_get_contents($questionFile)); + $result = $this->handler->import_question($sessionKey, self::$surveyId, $testGroupId, $question, 'lsq'); + + $this->assertIsArray($result, 'There was an error importing a question with a code that already exists.'); + } + + /** + * Importing a question with a question code that already exists. + * But set a new title + */ + public function testImportQuestionWithRepeatedQuestionCodeSetNew() + { + $sessionKey = $this->handler->get_session_key($this->getUsername(), $this->getPassword()); + + /** @var integer the only group id */ + $testGroupId = self::$testSurvey->groups[0]->gid; + $questionFile = self::$surveysFolder . '/limesurvey_question_import_question_test.lsq'; + $question = base64_encode(file_get_contents($questionFile)); + /* must return integer */ + $result = $this->handler->import_question( + $sessionKey, + self::$surveyId, + $testGroupId, + $question, + 'lsq', + 'N', + 'QNewTitle' + ); + $this->assertIsInt($result, 'There was an error importing a question with a code that already exists and new title is set.'); + /* Validate is set */ + $oQuestion = \Question::model()->find( + "qid = :qid", + array(':qid' => $result) + ); + $this->assertNotEmpty($oQuestion); + $this->assertEquals('QNewTitle', $oQuestion->title); + /* must return array */ + $result = $this->handler->import_question( + $sessionKey, + self::$surveyId, + $testGroupId, + $question, + 'lsq', + 'N', + 'QNewTitle' + ); + $this->assertIsArray($result, 'There was an error importing a question set a code that already exists.'); + } + + /** + * Importing a question with a question code that already exists. + * But set a new title + */ + public function testImportQuestionWithSetTextAndHelp() + { + $sessionKey = $this->handler->get_session_key($this->getUsername(), $this->getPassword()); + + /** @var integer the only group id */ + $testGroupId = self::$testSurvey->groups[0]->gid; + $questionFile = self::$surveysFolder . '/limesurvey_question_import_question_test.lsq'; + $question = base64_encode(file_get_contents($questionFile)); + $result = $this->handler->import_question( + $sessionKey, + self::$surveyId, + $testGroupId, + $question, + 'lsq', + 'N', + 'QNewTitle2', // new code + 'QNewText', // new quetsion text (all i10n) + 'QNewHelp' // new quetsion help (all i10n) + ); + + $this->assertIsInt($result, 'There was an error importing a question with a code that already exists and new title is set when set text and help.'); + $oQuestionL10n = \QuestionL10n::model()->find( + "qid = :qid and language = :language", + array(':qid' => $result, ':language' => "en") + ); + $this->assertNotEmpty($oQuestionL10n); + $this->assertEquals('QNewText', $oQuestionL10n->question); + $this->assertEquals('QNewHelp', $oQuestionL10n->help); + + } +}