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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 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);
+
+ }
+}