<?php

/**
 * This class handles all methods of the RemoteControl 2 API
 */

use LimeSurvey\PluginManager\PluginEvent;

class remotecontrol_handle
{
    const INVALID_SESSION_KEY = 'Invalid session key';

    /**
     * @var AdminController
     */
    protected $controller;

    /**
     * Constructor, stores the action instance into this handle class
     *
     * @access public
     * @param AdminController $controller
     */
    public function __construct(AdminController $controller)
    {
        $this->controller = $controller;
    }

    /**
     * Create and return a session key.
     *
     * Using this function you can create a new XML-RPC/JSON-RPC session key.
     * This is mandatory for all following LSRC2 function calls.
     *
     * * In case of success : Return the session key in string
     * * In case of error:
     *     * for protocol-level errors (invalid format etc), an error message.
     *     * For invalid username and password, returns a null error and the result body contains a 'status' name-value pair with the error message.
     *
     * @access public
     * @param string $username
     * @param string $password
     * @param string $plugin to be used
     * @return string|array
     */
    public function get_session_key($username, $password, $plugin = 'Authdb')
    {
        $username = (string) $username;
        $password = (string) $password;
        $loginResult = $this->_doLogin($username, $password, $plugin);
        if ($loginResult === true) {
            $this->_jumpStartSession($username);
            $sSessionKey = Yii::app()->securityManager->generateRandomString(32);
            $session = new Session();
            $session->id = $sSessionKey;
            $session->expire = time() + (int) Yii::app()->getConfig('iSessionExpirationTime', ini_get('session.gc_maxlifetime'));
            $session->data = $username;
            $session->save();
            return $sSessionKey;
        }
        if (is_string($loginResult)) {
            return array('status' => $loginResult);
        }
        return array('status' => 'Invalid user name or password');
    }

    /**
     * Close the RPC session
     *
     * Using this function you can close a previously opened XML-RPC/JSON-RPC session.
     *
     * @access public
     * @param string $sSessionKey the session key
     * @return string OK
     */
    public function release_session_key($sSessionKey)
    {
        $sSessionKey = (string) $sSessionKey;
        Session::model()->deleteAllByAttributes(array('id' => $sSessionKey));
        $criteria = new CDbCriteria();
        $criteria->condition = 'expire < ' . time();
        Session::model()->deleteAll($criteria);
        return 'OK';
    }

    /**
     * Get the available site settings
     *
     * Using this function you can get the available site settings.
     *
     * @access public
     * @param string $sSessionKey the session key
     * @return array
     */
    public function get_available_site_settings($sSessionKey)
    {
        $sSessionKey = (string) $sSessionKey;
        if (!$this->_checkSessionKey($sSessionKey)) {
            return array('status' => self::INVALID_SESSION_KEY);
        }

        if (!Permission::model()->hasGlobalPermission('superadmin', 'read')) {
            return array('status' => 'User is not allowed to read the available site settings');
        }

        return Yii::app()->getAvailableConfigs();
    }

    /**
     * Get a global setting
     *
     * Function to query site settings. Can only be used by super administrators.
     *
     * @access public
     * @param string $sSessionKey Auth Credentials
     * @param string $sSetttingName Name of the setting to get
     * @return string|array The requested value or an array with the error in case of error
     */
    public function get_site_settings($sSessionKey, $sSetttingName)
    {
        if ($this->_checkSessionKey($sSessionKey)) {
            if (Permission::model()->hasGlobalPermission('superadmin', 'read')) {
                $sSetttingName = (string) $sSetttingName;
                if (Yii::app()->getConfig($sSetttingName) !== false) {
                    return Yii::app()->getConfig($sSetttingName);
                } else {
                    return array('status' => 'Invalid setting');
                }
            } else {
                return array('status' => 'No permission');
            }
        } else {
            return array('status' => self::INVALID_SESSION_KEY);
        }
    }

    /* Survey specific functions */

    /**
     * Add an empty survey with minimum details
     *
     * This just tries to create an empty survey with the minimal settings.
     *
     * Failure status: Invalid session key, No permission, Faulty parameters, Creation Failed result
     *
     * @access public
     * @param string $sSessionKey Auth credentials
     * @param int $iSurveyID The desired ID of the Survey to add
     * @param string $sSurveyTitle Title of the new Survey
     * @param string $sSurveyLanguage Default language of the Survey
     * @param string $sformat (optional) Question appearance format (A, G or S) for "All on one page", "Group by Group", "Single questions", default to group by group (G)
     * @return int|array The survey ID in case of success
     */
    public function add_survey($sSessionKey, $iSurveyID, $sSurveyTitle, $sSurveyLanguage, $sformat = 'G')
    {
        $iSurveyID = (int) $iSurveyID;
        $sSurveyTitle = (string) $sSurveyTitle;
        $sSurveyLanguage = (string) $sSurveyLanguage;
        Yii::app()->loadHelper('surveytranslator');
        if ($this->_checkSessionKey($sSessionKey)) {
            if (Permission::model()->hasGlobalPermission('surveys', 'create')) {
                if ($sSurveyTitle == '' || $sSurveyLanguage == '' || !array_key_exists($sSurveyLanguage, getLanguageDataRestricted()) || !in_array($sformat, array('A', 'G', 'S'))) {
                    return array('status' => 'Faulty parameters');
                }

                $aInsertData = array(
                    'template' => App()->getConfig('defaulttheme'),
                    'owner_id' => Yii::app()->session['loginID'],
                    'active' => 'N',
                    'language' => $sSurveyLanguage,
                    'format' => $sformat
                );

                if (!is_null($iSurveyID)) {
                    $aInsertData['wishSID'] = $iSurveyID;
                }

                try {
                    $newSurvey = Survey::model()->insertNewSurvey($aInsertData);
                    if (!$newSurvey->sid) {
                        return array('status' => 'Creation Failed');  // status are a string, another way to send errors ?
                    }
                    $iNewSurveyid = $newSurvey->sid;

                    $sTitle = html_entity_decode($sSurveyTitle, ENT_QUOTES, 'UTF-8');

                    $aInsertData = array(
                        'surveyls_survey_id' => $iNewSurveyid,
                        'surveyls_title' => $sTitle,
                        'surveyls_language' => $sSurveyLanguage,
                    );

                    $langsettings = new SurveyLanguageSetting();
                    $langsettings->insertNewSurvey($aInsertData);
                    Permission::model()->giveAllSurveyPermissions(Yii::app()->session['loginID'], $iNewSurveyid);

                    return (int) $iNewSurveyid;
                } catch (Exception $e) {
                    return array('status' => $e->getmessage());
                }
            } else {
                return array('status' => 'No permission');
            }
        } else {
            return array('status' => self::INVALID_SESSION_KEY);
        }
    }

    /**
     * Delete a survey.
     *
     * Failure status: Invalid session key, No permission
     *
     * @access public
     * @param string $sSessionKey Auth credentials
     * @param int $iSurveyID The ID of the Survey to be deleted
     * @return array Returns status : status are OK in case of success
     */
    public function delete_survey($sSessionKey, $iSurveyID)
    {
        $iSurveyID = (int) $iSurveyID;
        if ($this->_checkSessionKey($sSessionKey)) {
            if (Permission::model()->hasSurveyPermission($iSurveyID, 'survey', 'delete')) {
                Survey::model()->deleteSurvey($iSurveyID, true);
                return array('status' => 'OK');
            } else {
                return array('status' => 'No permission');
            }
        } else {
            return array('status' => self::INVALID_SESSION_KEY);
        }
    }

    /**
     * Import survey in a known format (RPC function)
     *
     * Allow importing lss, csv, xls or survey zip archive in BASE 64 encoded.
     *
     * Failure status: Invalid session key, No permission, The import error
     *
     * @access public
     * @param string $sSessionKey Auth Credentials
     * @param string $sImportData String containing the BASE 64 encoded data of a lss, csv, txt or survey lsa archive
     * @param string $sImportDataType lss, csv, txt or lsa
     * @param string $sNewSurveyName (optional) The optional new name of the survey
     * @param integer $DestSurveyID  (optional) This is the new ID of the survey - if already used a random one will be taken instead
     * @return int|array The ID of the new survey in case of success
     */
    public function import_survey($sSessionKey, $sImportData, $sImportDataType, $sNewSurveyName = null, $DestSurveyID = null)
    {
        $sImportData = (string) $sImportData;
        $sNewSurveyName = (string) $sNewSurveyName;
        if (!is_null($DestSurveyID)) {
            $DestSurveyID = (int) $DestSurveyID;
        }
        if ($this->_checkSessionKey($sSessionKey)) {
            if (Permission::model()->hasGlobalPermission('surveys', 'create')) {
                if (!in_array($sImportDataType, array('lsa', 'csv', 'txt', 'lss'))) {
                    return array('status' => 'Invalid extension');
                }
                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)));
                $aImportResults = importSurveyFile($sFullFilePath, true, $sNewSurveyName, $DestSurveyID);
                unlink($sFullFilePath);
                if (isset($aImportResults['error']) && $aImportResults['error']) {
                    return array('status' => 'Error: ' . $aImportResults['error']);
                } else {
                    return (int) $aImportResults['newsid'];
                }
            } else {
                return array('status' => 'No permission');
            }
        } else {
            return array('status' => self::INVALID_SESSION_KEY);
        }
    }

    /**
     * Copy survey (RPC function)
     *
     * @access public
     * @param string $sSessionKey Auth credentials
     * @param int $iSurveyID_org Id of the source survey
     * @param string $sNewname name of the new survey
     * @param integer $DestSurveyID  (optional) This is the new ID of the survey - if already used a random one will be taken instead
     * @return array On success: new $iSurveyID in array['newsid']. On failure array with error information
     */
    public function copy_survey($sSessionKey, $iSurveyID_org, $sNewname, $DestSurveyID = null)
    {
        $iSurveyID = (int) $iSurveyID_org;
        if (!is_null($DestSurveyID)) {
            $DestSurveyID = (int) $DestSurveyID;
        }
        if (!$this->_checkSessionKey($sSessionKey)) {
            return array('status' => self::INVALID_SESSION_KEY);
        }
        if (!Permission::model()->hasGlobalPermission('surveys', 'create')) {
            return array(
                'status' => 'Copy failed',
                'error' => "You don't have sufficient permissions."
            );
        }
        if (!$iSurveyID) {
            return array(
                'status' => 'Copy failed',
                'error' => 'No survey ID has been provided. Cannot copy survey'
            );
        }
        if (!Survey::model()->findByPk($iSurveyID)) {
            return array(
                'status' => 'Copy failed',
                'error' => 'Invalid survey ID'
            );
        }
        if (!Permission::model()->hasSurveyPermission($iSurveyID, 'surveycontent', 'export')) {
            return array(
                'status' => 'Copy failed',
                'error' => "You don't have sufficient permissions"
            );
        }
        /* All check : try to copy */
        $aExcludes = array();
        $sNewSurveyName = $sNewname;
        $aExcludes['dates'] = true;
        $btranslinksfields = true;
        Yii::app()->loadHelper('export');
        $copysurveydata = surveyGetXMLData($iSurveyID, $aExcludes);
        if (empty($copysurveydata)) {
            return array(
                'status' => 'Copy failed',
                'error' => 'No data from survey'
            );
        }
        Yii::app()->loadHelper('admin.import');
        $aImportResults = XMLImportSurvey('', $copysurveydata, $sNewSurveyName, $DestSurveyID, $btranslinksfields);
        if (empty($aImportResults['newsid'])) {
            return array(
                'status' => 'Copy failed',
                'error' => $aImportResults['error']
            );
        }
        if (isset($aExcludes['conditions'])) {
            Question::model()->updateAll(array('relevance' => '1'), 'sid=' . $aImportResults['newsid']);
            QuestionGroup::model()->updateAll(array('grelevance' => '1'), 'sid=' . $aImportResults['newsid']);
        }
        if (!isset($aExcludes['permissions'])) {
            Permission::model()->copySurveyPermissions($iSurveyID, $aImportResults['newsid']);
        }
        return array(
            'status' => 'OK',
            'newsid' => $aImportResults['newsid']
        );
    }

    /**
     * Get survey properties (RPC function)
     *
     * Get properties of a survey
     * All internal properties of a survey are available.
     * @see \Survey for the list of available properties
     *
     * Failure status : Invalid survey ID, Invalid session key, No permission, No valid Data
     *
     * @access public
     * @param string $sSessionKey Auth credentials
     * @param int $iSurveyID The id of the Survey to be checked
     * @param array|null $aSurveySettings (optional) The properties to get
     * @return array
     */
    public function get_survey_properties($sSessionKey, $iSurveyID, $aSurveySettings = null)
    {
        Yii::app()->loadHelper('surveytranslator');
        if ($this->_checkSessionKey($sSessionKey)) {
            $iSurveyID = (int) $iSurveyID;
            $oSurvey = Survey::model()->findByPk($iSurveyID);
            if (!isset($oSurvey)) {
                return array('status' => 'Error: Invalid survey ID');
            }
            if (Permission::model()->hasSurveyPermission($iSurveyID, 'surveysettings', 'read')) {
                $aBasicDestinationFields = Survey::model()->tableSchema->columnNames;
                if (!empty($aSurveySettings)) {
                    $aSurveySettings = array_intersect($aSurveySettings, $aBasicDestinationFields);
                } else {
                    $aSurveySettings = $aBasicDestinationFields;
                }
                if (empty($aSurveySettings)) {
                    return array('status' => 'No valid Data');
                }
                $aResult = array();
                foreach ($aSurveySettings as $sPropertyName) {
                    $aResult[$sPropertyName] = $oSurvey->$sPropertyName;
                }
                return $aResult;
            } else {
                return array('status' => 'No permission');
            }
        } else {
            return array('status' => self::INVALID_SESSION_KEY);
        }
    }

    /**
     * Set survey properties (RPC function)
     *
     * @see \Survey for the list of available properties
     * Properties available are restricted
     * * Always
     *     * sid
     *     * active
     *     * language
     *     * additional_languages
     * * If survey is active
     *     * anonymized
     *     * datestamp
     *     * savetimings
     *     * ipaddr
     *     * refurl
     *
     * In case of partial success : return an array with key as properties and value as boolean , true if saved with success.
     *
     * Failure status : Invalid survey ID, Invalid session key, No permission, No valid Data
     *
     * @access public
     * @param string $sSessionKey Auth credentials
     * @param integer $iSurveyID - ID of the Survey
     * @param struct $aSurveyData - An array with the particular fieldnames as keys and their values to set on that particular Survey
     * @return array Of succeeded and failed nodifications according to internal validation
     */
    public function set_survey_properties($sSessionKey, $iSurveyID, $aSurveyData)
    {
        if ($this->_checkSessionKey($sSessionKey)) {
            $iSurveyID = (int) $iSurveyID;
            $oSurvey = Survey::model()->findByPk($iSurveyID);
            if (is_null($oSurvey)) {
                return array('status' => 'Error: Invalid survey ID');
            }
            if (Permission::model()->hasSurveyPermission($iSurveyID, 'surveysettings', 'update')) {
                // Remove fields that may not be modified
                unset($aSurveyData['sid']);
                // unset($aSurveyData['owner_id']);
                unset($aSurveyData['active']);
                unset($aSurveyData['language']);
                unset($aSurveyData['additional_languages']);
                // Remove invalid fields
                $aDestinationFields = array_flip(Survey::model()->tableSchema->columnNames);
                $aSurveyData = array_intersect_key($aSurveyData, $aDestinationFields);
                $oSurvey = Survey::model()->findByPk($iSurveyID);
                $aBasicAttributes = $oSurvey->getAttributes();
                $aResult = array();

                if ($oSurvey->isActive) {
                    // remove all fields that may not be changed when a survey is active
                    unset($aSurveyData['anonymized']);
                    unset($aSurveyData['datestamp']);
                    unset($aSurveyData['savetimings']);
                    unset($aSurveyData['ipaddr']);
                    unset($aSurveyData['refurl']);
                }

                if (empty($aSurveyData)) {
                    return array('status' => 'No valid Data');
                }

                foreach ($aSurveyData as $sFieldName => $sValue) {
                    $oSurvey->$sFieldName = $sValue;
                    try {
                        $bSaveResult = $oSurvey->save();  // save the change to database
                        // unset the value if it fails, so as to prevent future fails
                        $aResult[$sFieldName] = $bSaveResult;
                        if (!$bSaveResult) {
                            $oSurvey->$sFieldName = $aBasicAttributes[$sFieldName];
                        }
                    } catch (Exception $e) {
                        // unset the value that caused the exception
                        $oSurvey->$sFieldName = $aBasicAttributes[$sFieldName];
                    }
                }
                return $aResult;
            } else {
                return array('status' => 'No permission');
            }
        } else {
            return array('status' => self::INVALID_SESSION_KEY);
        }
    }

    /**
     * Activate survey (RPC function)
     *
     * Return the result of the activation
     * Failure status : Invalid Survey ID, Survey already active, Consistency check error, Activation Error, Invalid session key, No permission
     *
     * @access public
     * @param string $sSessionKey Auth credentials
     * @param int $iSurveyID ID of the Survey to be activated
     * @param array $activationSettings (optional) survey activation settings to change prior to activation, format is array of settingName => settingValue pairs (default: [])
     * @return array in case of success result of the activation
     */
    public function activate_survey($sSessionKey, $iSurveyID, $userActivationSettings = [])
    {
        // check provided session key and setup user session
        if (!$this->_checkSessionKey($sSessionKey)) {
            return array('status' => self::INVALID_SESSION_KEY);
        }
        $iSurveyID = (int) $iSurveyID;
        // check valid survey ID and current user has permission
        if (!Permission::model()->hasSurveyPermission($iSurveyID, 'surveyactivation', 'update')) {
            return array('status' => 'No permission');
        }
        $oSurvey = Survey::model()->findByPk($iSurveyID);

        // check if survey is already activated
        if ($oSurvey->isActive) {
            return array('status' => 'Error: Survey already active');
        }
        // Check consistency for groups and questions
        Yii::app()->loadHelper('admin.activate');
        $checkHasGroup = checkHasGroup($iSurveyID);
        $checkGroup = checkGroup($iSurveyID);
        if ($checkHasGroup !== false || $checkGroup !== false) {
            return array('status' => 'Error: Survey does not pass consistency check');
        }
        $surveyActivator = new SurveyActivator($oSurvey);
        // list of activation related survey settings and allowed values
        $activationSettingNames = [
            'anonymized',
            'datestamp',
            'ipaddr',
            'ipanonymize',
            'refurl',
            'savetimings',
        ];
        // update survey activation settings
        foreach ($activationSettingNames as $activationSettingName) {
            // use inherited value
            $oSurvey->{$activationSettingName} = $oSurvey->oOptions->{$activationSettingName};
            // override inherited value with user input
            if (isset($userActivationSettings[$activationSettingName]) && in_array($userActivationSettings[$activationSettingName], ['Y', 'N'])) {
                $oSurvey->{$activationSettingName} = $userActivationSettings[$activationSettingName];
            }
        }
        // persist changes
        $oSurvey->save();
        // attempt to activate the survey
        $aActivateResults = $surveyActivator->activate();
        if (isset($aActivateResults['error'])) {
            return array('status' => 'Error: ' . $aActivateResults['error']);
        }
        return $aActivateResults;
    }

    /**
     * Export survey statistics (RPC function)
     *
     * Allow to export statistics available Returns string - base64 encoding of the statistics.
     *
     * @access public
     * @param string $sSessionKey Auth credentials
     * @param int $iSurveyID ID of the Survey
     * @param string $docType (optional) Type of documents the exported statistics should be (pdf|xls|html)
     * @param string $sLanguage (optional) language of the survey to use (default from Survey)
     * @param string $graph (optional) Create graph option (default : no)
     * @param int|array $groupIDs (optional) array or integer containing the groups we choose to generate statistics from
     * @return string|array in case of success : Base64 encoded string with the statistics file
     */
    public function export_statistics($sSessionKey, $iSurveyID, $docType = 'pdf', $sLanguage = null, $graph = '0', $groupIDs = null)
    {
        Yii::app()->loadHelper('admin.statistics');

        if (!$this->_checkSessionKey($sSessionKey)) {
            return array('status' => self::INVALID_SESSION_KEY);
        }
        $iSurveyID = (int) $iSurveyID;
        $oSurvey = Survey::model()->findByPk($iSurveyID);
        if (!isset($oSurvey)) {
            return array('status' => 'Error: Invalid survey ID');
        };

        if (!Permission::model()->hasSurveyPermission($iSurveyID, 'statistics', 'read')) {
            return array('status' => 'Error: No Permission');
        }

        $aAdditionalLanguages = array_filter(explode(' ', (string) $oSurvey->additional_languages));

        if (is_null($sLanguage) || !in_array($sLanguage, $aAdditionalLanguages)) {
            $sLanguage = $oSurvey->language;
        }

        $oAllQuestions = Question::model()->getQuestionList($iSurveyID);
        if (!isset($oAllQuestions)) {
            return array('status' => 'No available data');
        }

        if ($groupIDs != null) {
            if (is_int($groupIDs)) {
                $groupIDs = array($groupIDs);
            }

            if (is_array($groupIDs)) {
                // check that every value of the array belongs to the survey defined
                $aGroups = QuestionGroup::model()->findAllByAttributes(array('sid' => $iSurveyID));

                foreach ($aGroups as $group) {
                    $validGroups[] = $group['gid'];
                }

                $groupIDs = array_intersect($groupIDs, $validGroups);

                if (empty($groupIDs)) {
                    return array('status' => 'Error: Invalid group ID');
                }

                foreach ($oAllQuestions as $key => $aQuestion) {
                    if (!in_array($aQuestion['gid'], $groupIDs)) {
                        unset($oAllQuestions[$key]);
                    }
                }
            } else {
                return array('status' => 'Error: Invalid group ID');
            }
        }

        if (!isset($oAllQuestions)) {
            return array('status' => 'No available data');
        }

        usort($oAllQuestions, 'groupOrderThenQuestionOrder');

        $aSummary = createCompleteSGQA($iSurveyID, $oAllQuestions, $sLanguage);

        $helper = new statistics_helper();
        switch ($docType) {
            case 'pdf':
                $sTempFile = $helper->generate_statistics($iSurveyID, $aSummary, $aSummary, $graph, $docType, 'F', $sLanguage);
                $sResult = file_get_contents($sTempFile);
                unlink($sTempFile);
                break;
            case 'xls':
                $sTempFile = $helper->generate_statistics($iSurveyID, $aSummary, $aSummary, '0', $docType, 'F', $sLanguage);
                $sResult = file_get_contents($sTempFile);
                unlink($sTempFile);
                break;
            case 'html':
                $sResult = $helper->generate_statistics($iSurveyID, $aSummary, $aSummary, '0', $docType, 'DD', $sLanguage);
                break;
        }

        return base64_encode((string) $sResult);
    }

    /**
     * Export submission timeline (RPC function)
     *
     * Returns an array of values (count and period)
     *
     * @access public
     * @param string $sSessionKey Auth credentials
     * @param int $iSurveyID ID of the Survey
     * @param string $sType (day|hour)
     * @param string $dStart
     * @param string $dEnd
     * @return array On success: The timeline. On failure array with error information
     */
    public function export_timeline($sSessionKey, $iSurveyID, $sType, $dStart, $dEnd)
    {
        $iSurveyID = (int) $iSurveyID;
        $survey = Survey::model()->findByPk($iSurveyID);

        if (!$this->_checkSessionKey($sSessionKey)) {
            return array('status' => self::INVALID_SESSION_KEY);
        }
        $iSurveyID = (int) $iSurveyID;
        if (is_null($survey)) {
            return array('status' => 'Error: Invalid survey ID');
        }
        if (!in_array($sType, array('day', 'hour'))) {
            return array('status' => 'Invalid Period');
        }
        if (!Permission::model()->hasSurveyPermission($iSurveyID, 'responses', 'read')) {
            return array('status' => 'No permission');
        }
        if (is_null($survey)) {
            return array('status' => 'Error: Invalid survey ID');
        }
        if (!tableExists($survey->responsesTableName)) {
            return array('status' => 'No available data');
        }

        $oResponses = SurveyDynamic::model($iSurveyID)->timeline($sType, $dStart, $dEnd);
        if (empty($oResponses)) {
            return array('status' => 'No valid Data');
        }

        return $oResponses;
    }

    /**
     * Get survey summary, regarding token usage and survey participation (RPC function)
     *
     * Returns the requested value as string, or all status in an array
     *
     * Available status are
     * * For Survey stats
     *     * completed_responses
     *     * incomplete_responses
     *     * full_responses
     * * For token part
     *     * token_count
     *     * token_invalid
     *     * token_sent
     *     * token_opted_out
     *     * token_completed
     *     * token_screenout
     * All available status can be sent using `all`
     *
     * Failure status : No available data, No such property, Invalid session key, No permission
     *
     * @access public
     * @param string $sSessionKey Auth credentials
     * @param int $iSurveyID ID of the Survey to get summary
     * @param string $sStatName (optional) Name of the summary option, or all to send all in an array (all by default)
     * @return string|array in case of success the requested value or an array of all values
     */
    public function get_summary($sSessionKey, $iSurveyID, $sStatName = 'all')
    {
        if ($this->_checkSessionKey($sSessionKey)) {
            $aPermittedTokenStats = array(
                'token_count',
                'token_invalid',
                'token_sent',
                'token_opted_out',
                'token_completed',
                'token_screenout'
            );
            $aPermittedSurveyStats = array(
                'completed_responses',
                'incomplete_responses',
                'full_responses'
            );
            $aPermittedStats = array_merge($aPermittedSurveyStats, $aPermittedTokenStats, array('all'));
            // Check if survey exists
            $iSurveyID = (int) $iSurveyID;
            $oSurvey = Survey::model()->findByPk($iSurveyID);
            if (!isset($oSurvey)) {
                return array('status' => 'Invalid surveyid');
            }

            if (!in_array($sStatName, $aPermittedStats)) {
                return array('status' => 'Invalid summary key');
            }

            // Check permissions to access this survey
            if (Permission::model()->hasSurveyPermission($iSurveyID, 'survey', 'read')) {
                $aSummary = array();

                if (in_array($sStatName, $aPermittedTokenStats) || $sStatName == 'all') {
                    if ($oSurvey->hasTokensTable) {
                        $aTokenSummary = Token::model($iSurveyID)->summary();
                        if ($aTokenSummary) {
                            $aSummary['token_count'] = $aTokenSummary['count'];
                            $aSummary['token_invalid'] = $aTokenSummary['invalid'];
                            $aSummary['token_sent'] = $aTokenSummary['sent'];
                            $aSummary['token_opted_out'] = $aTokenSummary['optout'];
                            $aSummary['token_completed'] = $aTokenSummary['completed'];
                            $aSummary['token_screenout'] = $aTokenSummary['screenout'];
                        }
                    } elseif ($sStatName != 'all') {
                        return array('status' => 'No available data');
                    }
                }

                if (in_array($sStatName, $aPermittedSurveyStats) || $sStatName == 'all') {
                    if (tableExists($oSurvey->responsesTableName)) {
                        $aSummary['completed_responses'] = SurveyDynamic::model($iSurveyID)->count('submitdate is NOT NULL');
                        $aSummary['incomplete_responses'] = SurveyDynamic::model($iSurveyID)->countByAttributes(array('submitdate' => null));
                        $aSummary['full_responses'] = SurveyDynamic::model($iSurveyID)->count();
                    } elseif ($sStatName != 'all') {
                        return array('status' => 'No available data');
                    }
                }

                if ($sStatName == 'all') {
                    return $aSummary;
                } else {
                    return $aSummary[$sStatName];
                }
            } else {
                return array('status' => 'No permission');
            }
        } else {
            return array('status' => self::INVALID_SESSION_KEY);
        }
    }

    /* Survey language specific functions */

    /**
     * Add a survey language (RPC function)
     *
     * @access public
     * @param string $sSessionKey Auth credentials
     * @param integer $iSurveyID ID of the Survey for which a survey participant list will be created
     * @param string $sLanguage  A valid language shortcut to add to the current Survey. If the language already exists no error will be given.
     * @return array Status=>OK when successful, otherwise the error description
     */
    public function add_language($sSessionKey, $iSurveyID, $sLanguage)
    {
        if ($this->_checkSessionKey($sSessionKey)) {
            $iSurveyID = (int) $iSurveyID;
            $oSurvey = Survey::model()->findByPk($iSurveyID);
            if (is_null($oSurvey)) {
                return array('status' => 'Error: Invalid survey ID');
            }
            if (Permission::model()->hasSurveyPermission($iSurveyID, 'surveysettings', 'update')) {
                Yii::app()->loadHelper('surveytranslator');
                $aLanguages = getLanguageData();

                if (!isset($aLanguages[$sLanguage])) {
                    return array('status' => 'Invalid language');
                }
                $oSurvey = Survey::model()->findByPk($iSurveyID);
                if ($sLanguage == $oSurvey->language) {
                    return array('status' => 'OK');
                }
                $aLanguages = $oSurvey->getAdditionalLanguages();
                $aLanguages[] = $sLanguage;
                $aLanguages = array_unique($aLanguages);
                $oSurvey->additional_languages = implode(' ', $aLanguages);
                try {
                    $oSurvey->save();  // save the change to database
                    $languagedetails = getLanguageDetails($sLanguage);

                    $insertdata = array(
                        'surveyls_survey_id' => $iSurveyID,
                        'surveyls_language' => $sLanguage,
                        'surveyls_title' => '',
                        'surveyls_dateformat' => $languagedetails['dateformat']
                    );
                    $setting = new SurveyLanguageSetting();
                    foreach ($insertdata as $k => $v) {
                        $setting->$k = $v;
                    }
                    $setting->save();
                    fixLanguageConsistency($iSurveyID, $sLanguage);
                    return array('status' => 'OK');
                } catch (Exception $e) {
                    return array('status' => 'Error');
                }
            } else {
                return array('status' => 'No permission');
            }
        }
        return null;
    }

    /**
     * Delete a language from a survey (RPC function)
     *
     * @access public
     * @param string $sSessionKey Auth credentials
     * @param integer $iSurveyID ID of the Survey for which a survey participant list will be created
     * @param string $sLanguage A valid language shortcut to delete from the current Survey. If the language does not exist in that Survey no error will be given.
     * @return array Status=>OK when successful, otherwise the error description
     */
    public function delete_language($sSessionKey, $iSurveyID, $sLanguage)
    {
        if ($this->_checkSessionKey($sSessionKey)) {
            $iSurveyID = (int) $iSurveyID;
            $oSurvey = Survey::model()->findByPk($iSurveyID);
            if (is_null($oSurvey)) {
                return array('status' => 'Error: Invalid survey ID');
            }

            if (Permission::model()->hasSurveyPermission($iSurveyID, 'surveysettings', 'update')) {
                Yii::app()->loadHelper('surveytranslator');
                $aLanguages = getLanguageData();

                if (!isset($aLanguages[$sLanguage])) {
                    return array('status' => 'Invalid language');
                }
                $oSurvey = Survey::model()->findByPk($iSurveyID);
                if ($sLanguage == $oSurvey->language) {
                    return array('status' => 'Cannot remove base language');
                }
                $aLanguages = $oSurvey->getAdditionalLanguages();
                $iLanguageKey = array_search($sLanguage, $aLanguages, true);
                unset($aLanguages[$iLanguageKey]);
                $oSurvey->additional_languages = implode(' ', $aLanguages);
                try {
                    $oSurvey->save();  // save the change to database
                    SurveyLanguageSetting::model()->deleteByPk(array('surveyls_survey_id' => $iSurveyID, 'surveyls_language' => $sLanguage));
                    cleanLanguagesFromSurvey($iSurveyID, $oSurvey->additional_languages);
                    return array('status' => 'OK');
                } catch (Exception $e) {
                    return array('status' => 'Error');
                }
            } else {
                return array('status' => 'No permission');
            }
        }
        return null;
    }

    /**
     * Get survey language properties (RPC function)
     *
     * @see \SurveyLanguageSetting for available properties
     *
     * @access public
     * @param string $sSessionKey Auth credentials
     * @param int $iSurveyID ID of the Survey
     * @param array|null $aSurveyLocaleSettings (optional) Properties to get, default to all attributes
     * @param string|null $sLang (optional) Language to use, default to Survey->language
     * @return array in case of success The requested values
     */
    public function get_language_properties($sSessionKey, $iSurveyID, $aSurveyLocaleSettings = null, $sLang = null)
    {
        Yii::app()->loadHelper('surveytranslator');
        if ($this->_checkSessionKey($sSessionKey)) {
            $iSurveyID = (int) $iSurveyID;
            $oSurvey = Survey::model()->findByPk($iSurveyID);
            if (!isset($oSurvey)) {
                return array('status' => 'Error: Invalid survey ID');
            }
            if (Permission::model()->hasSurveyPermission($iSurveyID, 'surveylocale', 'read')) {
                $aBasicDestinationFields = SurveyLanguageSetting::model()->tableSchema->columnNames;
                if (!empty($aSurveyLocaleSettings)) {
                    $aSurveyLocaleSettings = array_intersect($aSurveyLocaleSettings, $aBasicDestinationFields);
                } else {
                    $aSurveyLocaleSettings = $aBasicDestinationFields;
                }

                if ($sLang == null || !array_key_exists($sLang, getLanguageDataRestricted())) {
                    $sLang = $oSurvey->language;
                }

                $oSurveyLocale = SurveyLanguageSetting::model()->findByAttributes(array('surveyls_survey_id' => $iSurveyID, 'surveyls_language' => $sLang));
                $aResult = array();

                if (empty($aSurveyLocaleSettings)) {
                    return array('status' => 'No valid Data');
                }

                foreach ($aSurveyLocaleSettings as $sPropertyName) {
                    $aResult[$sPropertyName] = $oSurveyLocale->$sPropertyName;
                    // $aResult[$sPropertyName]=$aLangAttributes[$sPropertyName];
                }
                return $aResult;
            } else {
                return array('status' => 'No permission');
            }
        } else {
            return array('status' => self::INVALID_SESSION_KEY);
        }
    }

    /**
     * Set survey language properties (RPC function)
     *
     * @see \SurveyLanguageSetting for available properties.
     *
     * Some properties can not be set
     * * surveyls_language
     * * surveyls_survey_id
     *
     * @access public
     * @param string $sSessionKey Auth credentials
     * @param integer $iSurveyID  - ID of the Survey
     * @param struct $aSurveyLocaleData - An array with the particular fieldnames as keys and their values to set on that particular survey
     * @param string $sLanguage - Optional - Language to update  - if not given the base language of the particular survey is used
     * @return array in case of success 'status'=>'OK', when save successful otherwise error text.
     */
    public function set_language_properties($sSessionKey, $iSurveyID, $aSurveyLocaleData, $sLanguage = null)
    {
        Yii::app()->loadHelper('surveytranslator');
        if ($this->_checkSessionKey($sSessionKey)) {
            $iSurveyID = (int) $iSurveyID;
            $oSurvey = Survey::model()->findByPk($iSurveyID);
            if (is_null($oSurvey)) {
                return array('status' => 'Error: Invalid survey ID');
            }

            if (is_null($sLanguage)) {
                $sLanguage = $oSurvey->language;
            }

            if (!array_key_exists($sLanguage, getLanguageDataRestricted())) {
                return array('status' => 'Error: Invalid language');
            }

            if (Permission::model()->hasSurveyPermission($iSurveyID, 'surveylocale', 'update')) {
                // Remove fields that may not be modified
                unset($aSurveyLocaleData['surveyls_language']);
                unset($aSurveyLocaleData['surveyls_survey_id']);

                // Remove invalid fields
                $aDestinationFields = array_flip(SurveyLanguageSetting::model()->tableSchema->columnNames);

                $aSurveyLocaleData = array_intersect_key($aSurveyLocaleData, $aDestinationFields);
                $oSurveyLocale = SurveyLanguageSetting::model()->findByPk(array('surveyls_survey_id' => $iSurveyID, 'surveyls_language' => $sLanguage));

                $aLangAttributes = $oSurveyLocale->getAttributes();
                $aResult = array();

                if (empty($aSurveyLocaleData)) {
                    return array('status' => 'No valid Data');
                }

                foreach ($aSurveyLocaleData as $sFieldName => $sValue) {
                    $oSurveyLocale->$sFieldName = $sValue;
                    try {
                        // save the change to database - Every single change alone - to allow for validation to work
                        $bSaveResult = $oSurveyLocale->save();
                        $aResult[$sFieldName] = $bSaveResult;
                        // unset failed values
                        if (!$bSaveResult) {
                            $oSurveyLocale->$sFieldName = $aLangAttributes[$sFieldName];
                        }
                    } catch (Exception $e) {
                        $oSurveyLocale->$sFieldName = $aLangAttributes[$sFieldName];
                    }
                }
                $aResult['status'] = 'OK';
                return $aResult;
            } else {
                return array('status' => 'No permission');
            }
        } else {
            return array('status' => self::INVALID_SESSION_KEY);
        }
    }

    /* Group specific functions */

    /**
     * Add empty group with minimum details (RPC function)
     *
     * Used as a placeholder for importing questions.
     * Returns the groupid of the created group.
     *
     * @access public
     * @param string $sSessionKey Auth credentials
     * @param int $iSurveyID ID of the Survey to add the group
     * @param string $sGroupTitle Name of the group
     * @param string $sGroupDescription     Optional description of the group
     * @return array|int The id of the new group - Or status
     */
    public function add_group($sSessionKey, $iSurveyID, $sGroupTitle, $sGroupDescription = '')
    {
        if ($this->_checkSessionKey($sSessionKey)) {
            if (Permission::model()->hasSurveyPermission($iSurveyID, 'survey', 'update')) {
                $iSurveyID = (int) $iSurveyID;
                $oSurvey = Survey::model()->findByPk($iSurveyID);
                if (!isset($oSurvey)) {
                    return array('status' => 'Error: Invalid survey ID');
                }

                if ($oSurvey->isActive) {
                    return array('status' => 'Error:Survey is active and not editable');
                }

                $oGroup = new QuestionGroup();
                $oGroup->sid = $iSurveyID;
                $oGroup->group_order = getMaxGroupOrder($iSurveyID);
                if (!$oGroup->save()) {
                    return array('status' => 'Creation Failed');
                }

                $oQuestionGroupL10n = new QuestionGroupL10n();
                $oQuestionGroupL10n->group_name = $sGroupTitle;
                $oQuestionGroupL10n->description = $sGroupDescription;
                $oQuestionGroupL10n->language = Survey::model()->findByPk($iSurveyID)->language;
                $oQuestionGroupL10n->gid = $oGroup->gid;

                if ($oQuestionGroupL10n->save()) {
                    return (int) $oGroup->gid;
                } else {
                    return array('status' => 'Creation Failed');
                }
            } else {
                return array('status' => 'No permission');
            }
        } else {
            return array('status' => self::INVALID_SESSION_KEY);
        }
    }

    /**
     * Delete a group from a chosen survey (RPC function)
     *
     * Returns the id of the deleted group.
     *
     * @access public
     * @param string $sSessionKey Auth credentials
     * @param int $iSurveyID ID of the Survey that the group belongs
     * @param int $iGroupID ID of the group to delete
     * @return array|int The ID of the deleted group or status
     */
    public function delete_group($sSessionKey, $iSurveyID, $iGroupID)
    {
        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');
        }

        /*
         * Find with surveyid to avoid bad parameters and don't send information on group existence
         * @see https://bugs.limesurvey.org/view.php?id=19869
         */
        $oGroup = QuestionGroup::model()->findByAttributes(array('gid' => $iGroupID, 'sid' => $iSurveyID));
        if (!isset($oGroup)) {
            return array('status' => 'Error: Invalid group ID');
        }
        if (!Permission::model()->hasSurveyPermission($iSurveyID, 'surveycontent', 'delete')) {
            return array('status' => 'No permission');
        }
        if ($oSurvey->isActive) {
            return array('status' => 'Error:Survey is active and not editable');
        }
        $depented_on = getGroupDepsForConditions($oGroup->sid, 'all', $iGroupID, 'by-targgid');
        if (isset($depented_on)) {
            return array('status' => 'Group with depencdencies - deletion not allowed');
        }
        $iGroupsDeleted = QuestionGroup::deleteWithDependency($iGroupID);
        if ($iGroupsDeleted === 1) {
            QuestionGroup::model()->updateGroupOrder($iSurveyID);
            return (int) $iGroupID;
        } else {
            return array('status' => 'Group deletion failed');
        }
    }

    /**
     * Import a group and add to a survey (RPC function)
     *
     * @access public
     * @param string $sSessionKey Auth credentials
     * @param int $iSurveyID The ID of the Survey that the group will belong
     * @param string $sImportData String containing the BASE 64 encoded data of a lsg,csv
     * @param string $sImportDataType  lsg,csv
     * @param string $sNewGroupName  Optional new name for the group in the survey's base language
     * @param string $sNewGroupDescription  Optional new description for the group in the survey's base language
     * @return array|integer iGroupID  - ID of the new group or status
     */
    public function import_group($sSessionKey, $iSurveyID, $sImportData, $sImportDataType, $sNewGroupName = null, $sNewGroupDescription = null)
    {
        if ($this->_checkSessionKey($sSessionKey)) {
            $iSurveyID = (int) $iSurveyID;
            $sImportData = (string) $sImportData;
            $oSurvey = Survey::model()->findByPk($iSurveyID);
            if (!isset($oSurvey)) {
                return array('status' => 'Error: Invalid survey ID');
            }

            if (Permission::model()->hasSurveyPermission($iSurveyID, 'surveycontent', 'import')) {
                if ($oSurvey->isActive) {
                    return array('status' => 'Error:Survey is active and not editable');
                }

                if (!in_array($sImportDataType, array('csv', 'lsg'))) {
                    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)));

                if (strtolower($sImportDataType) == 'lsg') {
                    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 group structure XML ');
                    }
                    $aImportResults = XMLImportGroup($sFullFilePath, $iSurveyID, true);
                } else {
                    return array('status' => 'Invalid extension');
                }
                // just for symmetry!

                unlink($sFullFilePath);

                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 {
                    $iNewgid = $aImportResults['newgid'];

                    $oGroup = QuestionGroup::model()->findByAttributes(array('gid' => $iNewgid));
                    $sLanguage = Survey::model()->findByPk($iSurveyID)->language;
                    $oGroupL10n = $oGroup->questiongroupl10ns[$sLanguage];

                    if ($sNewGroupName != '') {
                        $oGroupL10n->setAttribute('group_name', (string) $sNewGroupName);
                    }
                    if ($sNewGroupDescription != '') {
                        $oGroupL10n->setAttribute('description', (string) $sNewGroupDescription);
                    }
                    try {
                        $oGroupL10n->save();
                    } catch (Exception $e) {
                        // no need to throw exception
                    }
                    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 (int) $aImportResults['newgid'];
                }
            } else {
                return array('status' => 'No permission');
            }
        } else {
            return array('status' => self::INVALID_SESSION_KEY);
        }
    }

    /**
     * Find response IDs given a survey ID and a token (RPC function)
     *
     * @param string $sSessionKey
     * @param int $iSurveyID
     * @param string $sToken
     * @return array
     */
    public function get_response_ids($sSessionKey, $iSurveyID, $sToken)
    {
        if ($this->_checkSessionKey($sSessionKey)) {
            if (!Permission::model()->hasSurveyPermission($iSurveyID, 'responses', 'read')) {
                return array('status' => 'No permission');
            }
            $iSurveyID = (int) $iSurveyID;
            $responses = SurveyDynamic::model($iSurveyID)->findAllByAttributes(array('token' => $sToken));
            $result = array();
            foreach ($responses as $response) {
                $result[] = (int) $response->id;
            }
            return $result;
        } else {
            return array('status' => self::INVALID_SESSION_KEY);
        }
    }

    /**
     * Get the properties of a group of a survey .
     *
     * Returns array of properties needed or all properties
     * @see \QuestionGroup for available properties
     *
     * @access public
     * @param string $sSessionKey Auth credentials
     * @param int $iGroupID Id of the group to get properties of
     * @param array  $aGroupSettings The properties to get
     * @param string $sLanguage Optional parameter language for multilingual groups
     * @return array in case of success the requested values in array
     */
    public function get_group_properties($sSessionKey, $iGroupID, $aGroupSettings = null, $sLanguage = null)
    {
        if ($this->_checkSessionKey($sSessionKey)) {
            $iGroupID = (int) $iGroupID;
            $oGroup = QuestionGroup::model()->with('questiongroupl10ns')->findByAttributes(array('gid' => $iGroupID));
            if (!isset($oGroup)) {
                return array('status' => 'Error: Invalid group ID');
            }

            if (Permission::model()->hasSurveyPermission($oGroup->sid, 'survey', 'read')) {
                $iSurveyID = $oGroup->sid;
                if (is_null($sLanguage)) {
                    $sLanguage = Survey::model()->findByPk($iSurveyID)->language;
                }

                if (!array_key_exists($sLanguage, getLanguageDataRestricted())) {
                    return array('status' => 'Error: Invalid language');
                }

                $aBasicDestinationFields = QuestionGroup::model()->tableSchema->columnNames;
                array_push($aBasicDestinationFields, 'group_name');
                array_push($aBasicDestinationFields, 'description');
                if (!empty($aGroupSettings)) {
                    $aGroupSettings = array_intersect($aGroupSettings, $aBasicDestinationFields);
                } else {
                    $aGroupSettings = $aBasicDestinationFields;
                }

                if (empty($aGroupSettings)) {
                    return array('status' => 'No valid Data');
                }

                foreach ($aGroupSettings as $sGroupSetting) {
                    if (isset($oGroup->$sGroupSetting)) {
                        $aResult[$sGroupSetting] = $oGroup->$sGroupSetting;
                    } elseif (
                        isset($oGroup->questiongroupl10ns[$sLanguage]) &&
                        isset($oGroup->questiongroupl10ns[$sLanguage]->$sGroupSetting)
                    ) {
                        $aResult[$sGroupSetting] = $oGroup->questiongroupl10ns[$sLanguage]->$sGroupSetting;
                    }
                }
                return $aResult;
            } else {
                return array('status' => 'No permission');
            }
        } else {
            return array('status' => self::INVALID_SESSION_KEY);
        }
    }

    /**
     * Set group properties (RPC function)
     *
     * @see \QuestionGroup for available properties and restriction
     *
     * Some attribute can not be set
     * * sid
     * * gid
     *
     * @access public
     * @param string $sSessionKey Auth credentials
     * @param integer $iGroupID  - ID of the Group
     * @param array $aGroupData - An array with the particular fieldnames as keys and their values to set on that particular survey
     * @return array Of succeeded and failed modifications according to internal validation.
     */
    public function set_group_properties($sSessionKey, $iGroupID, $aGroupData)
    {
        if ($this->_checkSessionKey($sSessionKey)) {
            $iGroupID = (int) $iGroupID;
            $oGroup = QuestionGroup::model()->with('questiongroupl10ns')->findByAttributes(array('gid' => $iGroupID));
            if (is_null($oGroup)) {
                return array('status' => 'Error: Invalid group ID');
            }
            if (Permission::model()->hasSurveyPermission($oGroup->sid, 'surveycontent', 'update')) {
                $aResult = array();
                // Remove fields that may not be modified
                unset($aGroupData['sid']);
                unset($aGroupData['gid']);

                // Backwards compatibility for L10n data
                if (!empty($aGroupData['language'])) {
                    $language = $aGroupData['language'];
                    $aGroupData['questiongroupl10ns'][$language] = array(
                        'language' => $language,
                        'group_name' => !empty($aGroupData['group_name']) ? $aGroupData['group_name'] : '',
                        'description' => !empty($aGroupData['description']) ? $aGroupData['description'] : '',
                    );
                }

                // Process L10n data
                if (!empty($aGroupData['questiongroupl10ns']) && is_array($aGroupData['questiongroupl10ns'])) {
                    $aL10nDestinationFields = array_flip(QuestionGroupL10n::model()->tableSchema->columnNames);
                    foreach ($aGroupData['questiongroupl10ns'] as $language => $aLanguageData) {
                        // Get existing L10n data or create new
                        if (isset($oGroup->questiongroupl10ns[$language])) {
                            $oQuestionGroupL10n = $oGroup->questiongroupl10ns[$language];
                        } else {
                            $oQuestionGroupL10n = new QuestionGroupL10n();
                            $oQuestionGroupL10n->gid = $iGroupID;
                            $oQuestionGroupL10n->setAttribute('language', $language);
                            $oQuestionGroupL10n->setAttribute('group_name', '');
                            $oQuestionGroupL10n->setAttribute('description', '');
                            if (!$oQuestionGroupL10n->save()) {
                                $aResult['questiongroupl10ns'][$language] = false;
                                continue;
                            }
                        }

                        // Remove invalid fields
                        $aGroupL10nData = array_intersect_key($aLanguageData, $aL10nDestinationFields);
                        if (empty($aGroupL10nData)) {
                            $aResult['questiongroupl10ns'][$language] = 'Empty group L10n data';
                            continue;
                        }

                        $aGroupL10nAttributes = $oQuestionGroupL10n->getAttributes();
                        foreach ($aGroupL10nData as $sFieldName => $sValue) {
                            $oQuestionGroupL10n->setAttribute($sFieldName, $sValue);
                            try {
                                // save the change to database - one by one to allow for validation to work
                                $bSaveResult = $oQuestionGroupL10n->save();
                                $aResult['questiongroupl10ns'][$language][$sFieldName] = $bSaveResult;
                                // unset failed values
                                if (!$bSaveResult) {
                                    $oQuestionGroupL10n->$sFieldName = $aGroupL10nAttributes[$sFieldName];
                                }
                            } catch (Exception $e) {
                                // unset values that cause exception
                                $oQuestionGroupL10n->$sFieldName = $aGroupL10nAttributes[$sFieldName];
                            }
                        }
                    }
                }

                // Remove invalid fields
                $aDestinationFields = array_flip(QuestionGroup::model()->tableSchema->columnNames);
                $aGroupData = array_intersect_key($aGroupData, $aDestinationFields);
                $aGroupAttributes = $oGroup->getAttributes();
                if (empty($aGroupData)) {
                    if (empty($aResult)) {
                        return array('status' => 'No valid Data');
                    } else {
                        return $aResult;
                    }
                }

                foreach ($aGroupData as $sFieldName => $sValue) {
                    // all dependencies this group has
                    $has_dependencies = getGroupDepsForConditions($oGroup->sid, $iGroupID);
                    // all dependencies on this group
                    $depented_on = getGroupDepsForConditions($oGroup->sid, 'all', $iGroupID, 'by-targgid');
                    // We do not allow groups with dependencies to change order - that would lead to broken dependencies

                    if ((isset($has_dependencies) || isset($depented_on)) && $sFieldName == 'group_order') {
                        $aResult[$sFieldName] = 'Group with dependencies - Order cannot be changed';
                        continue;
                    }
                    $oGroup->setAttribute($sFieldName, $sValue);

                    try {
                        // save the change to database - one by one to allow for validation to work
                        $bSaveResult = $oGroup->save();
                        QuestionGroup::model()->updateGroupOrder($oGroup->sid);
                        $aResult[$sFieldName] = $bSaveResult;
                        // unset failed values
                        if (!$bSaveResult) {
                            $oGroup->$sFieldName = $aGroupAttributes[$sFieldName];
                        }
                    } catch (Exception $e) {
                        // unset values that cause exception
                        $oGroup->$sFieldName = $aGroupAttributes[$sFieldName];
                    }
                }
                return $aResult;
            } else {
                return array('status' => 'No permission');
            }
        } else {
            return array('status' => self::INVALID_SESSION_KEY);
        }
    }

    /* Question specific functions */

    /**
     * Delete question from a survey (RPC function)
     *
     * Returns the id of the deleted question.
     *
     * @access public
     * @param string $sSessionKey Auth credentials
     * @param int $iQuestionID ID of the Question to delete
     * @return array|int ID of the deleted Question or status
     */
    public function delete_question($sSessionKey, $iQuestionID)
    {
        if ($this->_checkSessionKey($sSessionKey)) {
            $iQuestionID = (int) $iQuestionID;
            $oQuestion = Question::model()->findByPk($iQuestionID);
            if (!isset($oQuestion)) {
                return array('status' => 'Error: Invalid question ID');
            }

            $iSurveyID = $oQuestion['sid'];

            if (Permission::model()->hasSurveyPermission($iSurveyID, 'surveycontent', 'delete')) {
                $oSurvey = Survey::model()->findByPk($iSurveyID);

                if ($oSurvey->isActive) {
                    return array('status' => 'Survey is active and not editable');
                }

                $oCondition = Condition::model()->findAllByAttributes(array('cqid' => $iQuestionID));
                if (count($oCondition) > 0) {
                    return array('status' => 'Cannot delete Question. Others rely on this question');
                }

                LimeExpressionManager::RevertUpgradeConditionsToRelevance(null, $iQuestionID);

                try {
                    $oQuestion->delete();
                    return (int) $iQuestionID;
                } catch (Exception $e) {
                    return array('status' => $e->getMessage());
                }
            } else {
                return array('status' => 'No permission');
            }
        } else {
            return array('status' => self::INVALID_SESSION_KEY);
        }
    }

    /**
     * Import question (RPC function)
     *
     * Import a question from lsq file
     *
     * @access public
     * @param string $sSessionKey
     * @param int $iSurveyID The ID of the Survey that the question will belong to
     * @param int $iGroupID The ID of the Group that the question will belong to
     * @param string $sImportData String containing the BASE 64 encoded data of a lsq
     * @param string $sImportDataType  lsq
     * @param string $sMandatory (optional) Mandatory question option (default to No)
     * @param string $sNewQuestionTitle  (optional) new title for the question
     * @param string $sNewqQuestion (optional) new question text
     * @param string $sNewQuestionHelp (optional) new question help text
     * @return array|integer The id of the new question in case of success. Array if errors
     */
    public function import_question($sSessionKey, $iSurveyID, $iGroupID, $sImportData, $sImportDataType, $sMandatory = 'N', $sNewQuestionTitle = null, $sNewqQuestion = null, $sNewQuestionHelp = null)
    {
        $bOldEntityLoaderState = null;
        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');
        }

        $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');
        }

        libxml_use_internal_errors(true);
        Yii::app()->loadHelper('admin.import');

        // 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);

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

            if (!$oQuestion->save()) {
                return array(
                    'status' => 'Error when update question',
                    'errors' => $oQuestion->getErrors()
                );
            }

            $oQuestionL10ns = QuestionL10n::model()->findAll(
                'qid = :qid',
                array(':qid' => $iNewqid)
            );

            foreach ($oQuestionL10ns as $oQuestionL10n) {
                if (!empty($sNewqQuestion)) {
                    $oQuestionL10n->setAttribute('question', $sNewqQuestion);
                }
                if (!empty($sNewQuestionHelp)) {
                    $oQuestionL10n->setAttribute('help', $sNewQuestionHelp);
                }
                if (!$oQuestionL10n->save()) {
                    $errors[] = $oQuestionL10n->getErrors();
                }
            }

            if (!empty($errors)) {
                return array(
                    'status' => 'Error when update question',
                    'errors' => $errors
                );
            }

            return intval($iNewqid);
        }
    }

    /**
     * Get properties of a question in a survey.
     *
     * @see \Question for available properties.
     * Some more properties are available_answers, subquestions, attributes, attributes_lang, answeroptions, answeroptions_multiscale, defaultvalue
     *
     * @access public
     * @param string $sSessionKey Auth credentials
     * @param int $iQuestionID ID of the question to get properties
     * @param array $aQuestionSettings (optional) properties to get, default to all
     * @param string $sLanguage (optional) parameter language for multilingual questions, default are \Survey->language
     * @return array The requested values
     */
    public function get_question_properties($sSessionKey, $iQuestionID, $aQuestionSettings = null, $sLanguage = null)
    {
        if ($this->_checkSessionKey($sSessionKey)) {
            $iQuestionID = (int) $iQuestionID;
            Yii::app()->loadHelper('surveytranslator');
            $oQuestion = Question::model()->findByAttributes(array('qid' => $iQuestionID));
            if (!isset($oQuestion)) {
                return array('status' => 'Error: Invalid questionid');
            }
            $iSurveyID = $oQuestion->sid;
            if (Permission::model()->hasSurveyPermission($iSurveyID, 'surveycontent', 'read')) {
                if (empty($sLanguage)) {
                    $sLanguage = Survey::model()->findByPk($iSurveyID)->language;
                }

                if (!array_key_exists($sLanguage, getLanguageDataRestricted())) {
                    return array('status' => 'Error: Invalid language');
                }

                $oQuestion = Question::model()->with('questionl10ns')
                                              ->find(
                                                  't.qid = :qid and questionl10ns.language = :language',
                                                  array(':qid' => $iQuestionID, ':language' => $sLanguage)
                                              );
                if (!isset($oQuestion)) {
                    return array('status' => 'Error: Invalid questionid');
                }

                $aBasicDestinationFields = Question::model()->tableSchema->columnNames;
                /* Push questionl10ns data, like 3.X for less update of external scripts */
                array_push($aBasicDestinationFields, 'question');
                array_push($aBasicDestinationFields, 'help');
                array_push($aBasicDestinationFields, 'script');
                /* Push questionl10ns data but complete */
                array_push($aBasicDestinationFields, 'questionl10ns');
                /* Other fileds */
                array_push($aBasicDestinationFields, 'available_answers');
                array_push($aBasicDestinationFields, 'subquestions');
                array_push($aBasicDestinationFields, 'attributes');
                array_push($aBasicDestinationFields, 'attributes_lang');

                if ($oQuestion->getIsDualScale()) {
                    array_push($aBasicDestinationFields, 'answeroptions_multiscale');
                } else {
                    array_push($aBasicDestinationFields, 'answeroptions');
                }

                array_push($aBasicDestinationFields, 'defaultvalue');
                if (!empty($aQuestionSettings)) {
                    $aQuestionSettings = array_intersect($aQuestionSettings, $aBasicDestinationFields);
                } else {
                    $aQuestionSettings = $aBasicDestinationFields;
                }

                if (empty($aQuestionSettings)) {
                    return array('status' => 'No valid Data');
                }

                $aResult = array();
                foreach ($aQuestionSettings as $sPropertyName) {
                    if ($sPropertyName == 'available_answers' || $sPropertyName == 'subquestions') {
                        $oSubQuestions = Question::model()->with('questionl10ns')
                                                          ->findAll(
                                                              't.parent_qid = :parent_qid and questionl10ns.language = :language',
                                                              array(':parent_qid' => $iQuestionID, ':language' => $sLanguage),
                                                              array('order' => 'title')
                                                          );

                        if (count($oSubQuestions) > 0) {
                            $aData = array();
                            foreach ($oSubQuestions as $oSubQuestion) {
                                if ($sPropertyName == 'available_answers') {
                                    $aData[$oSubQuestion['title']] = array_key_exists($sLanguage, $oSubQuestion->questionl10ns) ? $oSubQuestion->questionl10ns[$sLanguage]->question : '';
                                } else {
                                    $aData[$oSubQuestion['qid']]['title'] = $oSubQuestion['title'];
                                    $aData[$oSubQuestion['qid']]['question'] = array_key_exists($sLanguage, $oSubQuestion->questionl10ns) ? $oSubQuestion->questionl10ns[$sLanguage]->question : '';
                                    $aData[$oSubQuestion['qid']]['scale_id'] = $oSubQuestion['scale_id'];
                                }
                            }

                            $aResult[$sPropertyName] = $aData;
                        } else {
                            $aResult[$sPropertyName] = 'No available answers';
                        }
                    } elseif ($sPropertyName == 'attributes') {
                        $questionAttributeHelper = new LimeSurvey\Models\Services\QuestionAttributeHelper();
                        $questionAttributes = $questionAttributeHelper->getQuestionAttributesWithValues($oQuestion, null, null, true);
                        $data = [];
                        foreach ($questionAttributes as $attributeName => $attributeData) {
                            if (empty($attributeData['i18n'])) {
                                $data[$attributeName] = $attributeData['value'];
                            }
                        }
                        if (count($data) > 0) {
                            ksort($data, SORT_NATURAL | SORT_FLAG_CASE);
                            $aResult['attributes'] = $data;
                        } else {
                            $aResult['attributes'] = 'No available attributes';
                        }
                    } elseif ($sPropertyName == 'attributes_lang') {
                        $questionAttributeHelper = new LimeSurvey\Models\Services\QuestionAttributeHelper();
                        $questionAttributes = $questionAttributeHelper->getQuestionAttributesWithValues($oQuestion, $sLanguage, null, true);
                        $data = [];
                        foreach ($questionAttributes as $attributeName => $attributeData) {
                            if (!empty($attributeData['i18n'])) {
                                $data[$attributeName] = $attributeData[$sLanguage]['value'];
                            }
                        }
                        if (count($data) > 0) {
                            ksort($data, SORT_NATURAL | SORT_FLAG_CASE);
                            $aResult['attributes_lang'] = $data;
                        } else {
                            $aResult['attributes_lang'] = 'No available attributes';
                        }
                    } elseif ($sPropertyName == 'answeroptions') {
                        $oAttributes = Answer::model()->with('answerl10ns')
                                                      ->findAll(
                                                          't.qid = :qid and answerl10ns.language = :language',
                                                          array(':qid' => $iQuestionID, ':language' => $sLanguage),
                                                          array('order' => 'sortorder')
                                                      );
                        if (count($oAttributes) > 0) {
                            $aData = array();
                            foreach ($oAttributes as $oAttribute) {
                                $aData[$oAttribute['code']]['answer'] = array_key_exists($sLanguage, $oAttribute->answerl10ns) ? $oAttribute->answerl10ns[$sLanguage]->answer : '';
                                $aData[$oAttribute['code']]['assessment_value'] = $oAttribute['assessment_value'];
                                $aData[$oAttribute['code']]['scale_id'] = $oAttribute['scale_id'];
                                $aData[$oAttribute['code']]['order'] = $oAttribute['sortorder'];
                            }
                            $aResult['answeroptions'] = $aData;
                        } else {
                            $aResult['answeroptions'] = 'No available answer options';
                        }
                    } elseif ($sPropertyName == 'answeroptions_multiscale') {
                        $oAttributes = \Answer::model()->with('answerl10ns')
                                                       ->findAll(
                                                           't.qid = :qid and answerl10ns.language = :language',
                                                           array(':qid' => $iQuestionID, ':language' => $sLanguage),
                                                           array('order' => 'sortorder')
                                                       );

                        if (count($oAttributes) > 0) {
                            $aData = array();
                            foreach ($oAttributes as $oAttribute) {
                                $aData[$oAttribute['scale_id']][$oAttribute['code']]['code'] = $oAttribute['code'];
                                $aData[$oAttribute['scale_id']][$oAttribute['code']]['answer'] = array_key_exists($sLanguage, $oAttribute->answerl10ns) ? $oAttribute->answerl10ns[$sLanguage]->answer : '';
                                $aData[$oAttribute['scale_id']][$oAttribute['code']]['assessment_value'] = $oAttribute['assessment_value'];
                                $aData[$oAttribute['scale_id']][$oAttribute['code']]['scale_id'] = $oAttribute['scale_id'];
                                $aData[$oAttribute['scale_id']][$oAttribute['code']]['order'] = $oAttribute['sortorder'];
                            }
                            $aResult['answeroptions'] = $aData;
                        } else {
                            $aResult['answeroptions'] = 'No available answer options';
                        }
                    } elseif ($sPropertyName == 'defaultvalue') {
                        $aResult['defaultvalue'] = DefaultValue::model()->with('defaultvaluel10ns')
                                                                        ->find(
                                                                            'qid = :qid AND defaultvaluel10ns.language = :language',
                                                                            array(':qid' => $iQuestionID, ':language' => $sLanguage)
                                                                        )
                                                                        ->defaultvalue;
                    } elseif ($sPropertyName == 'question' || $sPropertyName == 'help' || $sPropertyName == 'script') {
                        $aResult[$sPropertyName] = $oQuestion->questionl10ns[$sLanguage]->$sPropertyName;
                    } elseif ($sPropertyName == 'questionl10ns') {
                        $aResult[$sPropertyName] = $oQuestion->questionl10ns[$sLanguage]->attributes;
                    } else {
                        $aResult[$sPropertyName] = $oQuestion->$sPropertyName;
                    }
                }
                return $aResult;
            } else {
                return array('status' => 'No permission');
            }
        } else {
            return array('status' => self::INVALID_SESSION_KEY);
        }
    }

    /**
     * Set question properties.
     *
     * @see \Question for available properties.
     *
     * Restricted properties:
     * * qid
     * * gid
     * * sid
     * * parent_qid
     * * language
     * * type
     * * question_order in some condition (with dependecies)
     *
     * @access public
     * @param string $sSessionKey Auth credentials
     * @param integer $iQuestionID  - ID of the question
     * @param array $aQuestionData - An array with the particular fieldnames as keys and their values to set on that particular question
     * @param string $sLanguage Optional parameter language for multilingual questions
     * @return array Of succeeded and failed modifications according to internal validation.
     */
    public function set_question_properties($sSessionKey, $iQuestionID, $aQuestionData, $sLanguage = null)
    {
        if ($this->_checkSessionKey($sSessionKey)) {
            Yii::app()->loadHelper('surveytranslator');
            $iQuestionID = (int) $iQuestionID;
            $oQuestion = Question::model()->with('questionl10ns')->findByAttributes(array('qid' => $iQuestionID));
            if (is_null($oQuestion)) {
                return array('status' => 'Error: Invalid group ID');
            }

            $iSurveyID = $oQuestion->sid;

            if (Permission::model()->hasSurveyPermission($iSurveyID, 'surveycontent', 'update')) {
                if (is_null($sLanguage)) {
                    $sLanguage = Survey::model()->findByPk($iSurveyID)->language;
                }

                if (!array_key_exists($sLanguage, getLanguageDataRestricted())) {
                    return array('status' => 'Error: Invalid language');
                }

                $oQuestion = Question::model()->findByAttributes(array('qid' => $iQuestionID));
                if (!isset($oQuestion)) {
                    return array('status' => 'Error: Invalid questionid');
                }

                // Backwards compatibility for L10n data
                if (!empty($aQuestionData['language'])) {
                    $language = $aQuestionData['language'];
                    $aQuestionData['questionl10ns'][$language] = array(
                        'language' => $language,
                        'question' => $aQuestionData['question'] ?? '',
                        'help' => $aQuestionData['help'] ?? '',
                    );
                }

                // Process L10n data
                if (!empty($aQuestionData['questionl10ns']) && is_array($aQuestionData['questionl10ns'])) {
                    $aL10nDestinationFields = array_flip(QuestionL10n::model()->tableSchema->columnNames);
                    foreach ($aQuestionData['questionl10ns'] as $language => $aLanguageData) {
                        // Get existing L10n data or create new
                        if (isset($oQuestion->questionl10ns[$language])) {
                            $oQuestionL10n = $oQuestion->questionl10ns[$language];
                        } else {
                            $oQuestionL10n = new QuestionL10n();
                            $oQuestionL10n->qid = $iQuestionID;
                            $oQuestionL10n->setAttribute('language', $language);
                            $oQuestionL10n->setAttribute('question', '');
                            $oQuestionL10n->setAttribute('help', '');
                            if (!$oQuestionL10n->save()) {
                                $aResult['questionl10ns'][$language] = false;
                                continue;
                            }
                        }

                        // Remove invalid fields
                        $aQuestionL10nData = array_intersect_key($aLanguageData, $aL10nDestinationFields);
                        if (empty($aQuestionL10nData)) {
                            $aResult['questionl10ns'][$language] = 'Empty question L10n data';
                            continue;
                        }

                        $aQuestionL10nAttributes = $oQuestionL10n->getAttributes();
                        foreach ($aQuestionL10nData as $sFieldName => $sValue) {
                            $oQuestionL10n->setAttribute($sFieldName, $sValue);
                            try {
                                // save the change to database - one by one to allow for validation to work
                                $bSaveResult = $oQuestionL10n->save();
                                $aResult['questionl10ns'][$language][$sFieldName] = $bSaveResult;
                                // unset failed values
                                if (!$bSaveResult) {
                                    $oQuestionL10n->$sFieldName = $aQuestionL10nAttributes[$sFieldName];
                                }
                            } catch (Exception $e) {
                                // unset values that cause exception
                                $oQuestionL10n->$sFieldName = $aQuestionL10nAttributes[$sFieldName];
                            }
                        }
                    }
                }

                // Remove fields that may not be modified
                unset($aQuestionData['qid']);
                unset($aQuestionData['gid']);
                unset($aQuestionData['sid']);
                unset($aQuestionData['parent_qid']);
                unset($aQuestionData['language']);
                unset($aQuestionData['type']);
                // Remove invalid fields
                $aDestinationFields = array_flip(Question::model()->tableSchema->columnNames);
                $aQuestionData = array_intersect_key($aQuestionData, $aDestinationFields);
                $aQuestionAttributes = $oQuestion->getAttributes();

                if (empty($aQuestionData)) {
                    if (empty($aResult)) {
                        return array('status' => 'No valid Data');
                    } else {
                        return $aResult;
                    }
                }

                foreach ($aQuestionData as $sFieldName => $sValue) {
                    // all the dependencies that this question has to other questions
                    $dependencies = getQuestDepsForConditions($oQuestion->sid, $oQuestion->gid, $iQuestionID);
                    // all dependencies by other questions to this question
                    $is_criteria_question = getQuestDepsForConditions($oQuestion->sid, $oQuestion->gid, 'all', $iQuestionID, 'by-targqid');
                    // We do not allow questions with dependencies in the same group to change order - that would lead to broken dependencies

                    if ((isset($dependencies) || isset($is_criteria_question)) && $sFieldName == 'question_order') {
                        $aResult[$sFieldName] = 'Questions with dependencies - Order cannot be changed';
                        continue;
                    }
                    $oQuestion->setAttribute($sFieldName, $sValue);

                    try {
                        $bSaveResult = $oQuestion->save();  // save the change to database
                        Question::model()->updateQuestionOrder($oQuestion->gid);
                        $aResult[$sFieldName] = $bSaveResult;
                        // unset fields that failed
                        if (!$bSaveResult) {
                            $oQuestion->$sFieldName = $aQuestionAttributes[$sFieldName];
                        }
                    } catch (Exception $e) {
                        // unset fields that caused exception
                        $oQuestion->$sFieldName = $aQuestionAttributes[$sFieldName];
                    }
                }
                return $aResult;
            } else {
                return array('status' => 'No permission');
            }
        } else {
            return array('status' => self::INVALID_SESSION_KEY);
        }
    }

    /* Participant-Token specific functions */

    /**
     * Add participants to the survey.
     *
     * The parameters $aParticipantData is a 2 dimensionnal array containing needed participant data.
     * @see \Token for all available attribute,
     * @example : `[ {"email":"me@example.com","lastname":"Bond","firstname":"James"},{"email":"me2@example.com","attribute_1":"example"} ]`
     *
     * Returns the inserted data including additional new information like the Token entry ID and the token string. In case of errors in some data, return it in errors.
     *
     * @access public
     * @param string $sSessionKey Auth credentials
     * @param int $iSurveyID ID of the Survey
     * @param array $aParticipantData Data of the participants to be added
     * @param bool $bCreateToken Optional - Defaults to true and determins if the access token automatically created
     * @return array The values added
     */
    public function add_participants($sSessionKey, $iSurveyID, $aParticipantData, $bCreateToken = true)
    {
        if (!$this->_checkSessionKey($sSessionKey)) {
            return array('status' => self::INVALID_SESSION_KEY);
        }
        $iSurveyID = (int) $iSurveyID;
        $oSurvey = Survey::model()->findByPk($iSurveyID);
        if (is_null($oSurvey)) {
            return array('status' => 'Error: Invalid survey ID');
        }

        if (Permission::model()->hasSurveyPermission($iSurveyID, 'tokens', 'create')) {
            if (!Yii::app()->db->schema->getTable($oSurvey->tokensTableName)) {
                return array('status' => 'No survey participant list');
            }
            $aDestinationFields = array_flip(Token::model($iSurveyID)->getMetaData()->tableSchema->columnNames);
            foreach ($aParticipantData as &$aParticipant) {
                $token = Token::create($iSurveyID);
                $token->setAttributes(array_intersect_key($aParticipant, $aDestinationFields));
                if ($bCreateToken) {
                    $token->generateToken();
                }
                if ($token->encryptSave(true)) {
                    $aParticipant = $token->getAttributes();
                } else {
                    $aParticipant['errors'] = $token->errors;
                }
            }
            return $aParticipantData;
        } else {
            return array('status' => 'No permission');
        }
    }

    /**
     * Delete multiple participants from the survey participant list (RPC function)
     *
     * Returns the id of the deleted token
     *
     * @access public
     * @param string $sSessionKey Auth credentials
     * @param int $iSurveyID ID of the Survey that the participants belong to
     * @param array $aTokenIDs ID of the tokens/participants to delete
     * @return array Result of deletion
     */
    public function delete_participants($sSessionKey, $iSurveyID, $aTokenIDs)
    {
        if ($this->_checkSessionKey($sSessionKey)) {
            $iSurveyID = (int) $iSurveyID;
            $oSurvey = Survey::model()->findByPk($iSurveyID);
            if (!isset($oSurvey)) {
                return array('status' => 'Error: Invalid survey ID');
            }

            if (Permission::model()->hasSurveyPermission($iSurveyID, 'tokens', 'delete')) {
                if (!tableExists("{{tokens_$iSurveyID}}")) {
                    return array('status' => 'Error: No survey participant list');
                }

                $aResult = array();
                foreach ($aTokenIDs as $iTokenID) {
                    $iTokenID = (int) $iTokenID;
                    $token = Token::model($iSurveyID)->findByPk($iTokenID);
                    if (!isset($token)) {
                        $aResult[$iTokenID] = 'Invalid token ID';
                    } else {
                        $iDel = Token::model($iSurveyID)->deleteByPk($iTokenID);
                        if ($iDel > 0) {
                            $aResult[$iTokenID] = 'Deleted';
                        } else {
                            $aResult[$iTokenID] = 'Deletion went wrong';
                        }
                    }
                }
                return $aResult;
            } else {
                return array('status' => 'No permission');
            }
        } else {
            return array('status' => self::INVALID_SESSION_KEY);
        }
    }

    /**
     * Get settings of a survey participant (RPC function)
     *
     * Allow to request for a specific participant. If more than one participant is returned with specified attribute(s) an error is returned.
     *
     * @access public
     * @param string $sSessionKey Auth credentials
     * @param int $iSurveyID ID of the Survey to get token properties
     * @param array|int $aTokenQueryProperties of participant properties used to query the participant, or the token id as an integer
     * @param array $aTokenProperties The properties to get
     * @return array The requested values
     */
    public function get_participant_properties($sSessionKey, $iSurveyID, $aTokenQueryProperties, $aTokenProperties = null)
    {
        if ($this->_checkSessionKey($sSessionKey)) {
            $iSurveyID = (int) $iSurveyID;
            $surveyidExists = Survey::model()->findByPk($iSurveyID);
            if (!isset($surveyidExists)) {
                return array('status' => 'Error: Invalid survey ID');
            }

            if (Permission::model()->hasSurveyPermission($iSurveyID, 'tokens', 'read')) {
                if (!tableExists("{{tokens_$iSurveyID}}")) {
                    return array('status' => 'Error: No survey participant list');
                }

                if (is_array($aTokenQueryProperties)) {
                    $tokenCount = Token::model($iSurveyID)->countByAttributes($aTokenQueryProperties);
                    if ($tokenCount == 0) {
                        return array('status' => 'Error: No results were found based on your attributes.');
                    } elseif ($tokenCount > 1) {
                        return array('status' => 'Error: More than 1 result was found based on your attributes.');
                    }
                    $token = Token::model($iSurveyID)->findByAttributes($aTokenQueryProperties);
                } else {
                    // If aTokenQueryProperties is not an array, but an integer
                    $iTokenID = $aTokenQueryProperties;
                    $token = Token::model($iSurveyID)->findByPk($iTokenID);
                }
                if (!isset($token)) {
                    return array('status' => 'Error: Invalid tokenid');
                }
                $token->decrypt();
                if (!empty($aTokenProperties)) {
                    $result = array_intersect_key($token->attributes, array_flip($aTokenProperties));
                } else {
                    $result = $token->attributes;
                }
                if (empty($result)) {
                    return array('status' => 'No valid Data');
                } else {
                    return $result;
                }
            } else {
                return array('status' => 'No permission');
            }
        } else {
            return array('status' => self::INVALID_SESSION_KEY);
        }
    }

    /**
     * Set properties of a survey participant (RPC function)
     *
     * Allow to set properties about a specific participant, only one particpant can be updated.
     * @see \Token for available properties
     *
     * @access public
     * @param string $sSessionKey Auth credentials
     * @param int $iSurveyID Id of the Survey that participants belong
     * @param struct|int $aTokenQueryProperties of participant properties used to query the participant, or the token id as an integer
     * @param struct $aTokenData An array with the particular fieldnames as keys and their values to set on that particular Participant
     * @return array Result of the change action
     */
    public function set_participant_properties($sSessionKey, $iSurveyID, $aTokenQueryProperties, $aTokenData)
    {
        if ($this->_checkSessionKey($sSessionKey)) {
            $iSurveyID = (int) $iSurveyID;
            $oSurvey = Survey::model()->findByPk($iSurveyID);
            if (!isset($oSurvey)) {
                return array('status' => 'Error: Invalid survey ID');
            }

            if (Permission::model()->hasSurveyPermission($iSurveyID, 'tokens', 'update')) {
                if (!tableExists("{{tokens_$iSurveyID}}")) {
                    return array('status' => 'Error: No survey participant list');
                }

                if (is_array($aTokenQueryProperties)) {
                    $oTokens = Token::model($iSurveyID)->findAllByAttributes($aTokenQueryProperties);
                    $tokenCount = count($oTokens);
                    if ($tokenCount == 0) {
                        return array('status' => 'Error: No results were found based on your attributes.');
                    } elseif ($tokenCount > 1) {
                        return array('status' => 'Error: More than 1 result was found based on your attributes.');
                    }
                    $oToken = $oTokens[0];
                } else {
                    // If aTokenQueryProperties is not an array but an integer
                    $iTokenID = $aTokenQueryProperties;
                    $oToken = Token::model($iSurveyID)->findByPk($iTokenID);
                }
                if (!isset($oToken)) {
                    return array('status' => 'Error: Invalid tokenid');
                }
                $oToken->decrypt();

                // Remove fields that may not be modified
                unset($aTokenData['tid']);

                $aBasicDestinationFields = array_flip($oToken->getTableSchema()->columnNames);
                $aTokenData = array_intersect_key($aTokenData, $aBasicDestinationFields);

                if (empty($aTokenData)) {
                    return array('status' => 'No valid Data');
                }

                $oToken->setAttributes($aTokenData, false);
                if ($oToken->encryptSave(true)) {
                    return $oToken->attributes;
                } else {
                    return array('status' => $oToken->getErrors());
                }
            } else {
                return array('status' => 'No permission');
            }
        } else {
            return array('status' => self::INVALID_SESSION_KEY);
        }
    }

    /**
     * Get survey groups (RPC function)
     *
     * Returns the ids and all attributes of all survey groups
     *
     * @access public
     * @param string $sSessionKey Auth credentials
     * @param int $iSurveyID ID of the Survey containing the groups
     * @param string $sLanguage Optional parameter language for multilingual groups
     * @return array in case of success the list of groups
     */
    public function list_groups($sSessionKey, $iSurveyID, $sLanguage = null)
    {
        if ($this->_checkSessionKey($sSessionKey)) {
            $iSurveyID = (int) $iSurveyID;
            $oSurvey = Survey::model()->findByPk($iSurveyID);
            if (!isset($oSurvey)) {
                return array('status' => 'Error: Invalid survey ID');
            }

            if (Permission::model()->hasSurveyPermission($iSurveyID, 'surveycontent', 'read')) {
                $oGroupList = QuestionGroup::model()->with('questiongroupl10ns')->findAllByAttributes(array('sid' => $iSurveyID));
                if (count($oGroupList) == 0) {
                    return array('status' => 'No groups found');
                }

                if (is_null($sLanguage)) {
                    $sLanguage = $oSurvey->language;
                }

                foreach ($oGroupList as $oGroup) {
                    $L10ns = $oGroup->questiongroupl10ns[$sLanguage];
                    $tmp = array('id' => $oGroup->primaryKey) + $oGroup->attributes;
                    $tmp['group_name'] = $L10ns['group_name'];
                    $tmp['description'] = $L10ns['description'];
                    $tmp['language'] = $sLanguage;
                    $aData[] = $tmp;
                }
                return $aData;
            } else {
                return array('status' => 'No permission');
            }
        } else {
            return array('status' => self::INVALID_SESSION_KEY);
        }
    }

    /**
     * Return the IDs and properties of survey participants (RPC function)
     *
     * If $bUnused is true, user will get the list of uncompleted tokens (token_return functionality).
     * If $bUnused is omitted or false, user will get the list of all tokens.
     * Parameters iStart and iLimit are used to limit the number of results of this call.
     * Starting with version 4.3.0 it is not possible anymore to query for several IDs just using
     * an array of values - instead you have use the 'IN' operator.
     * Examples of conditions:
     *     array ('tid => 'IN','1','3','26')
     *     array('email' => 'info@example.com')
     *     array('validuntil' => array('>', '2019-01-01 00:00:00'))
     *
     * By default return each participant with basic information
     * * tid : the token id
     * * token : the token for this participant
     * * participant_info : an array with firstname, lastname and email
     * Parameter $aAttributes can be used to add more attribute in participant_info array
     *
     * @access public
     * @param string $sSessionKey Auth credentials
     * @param int $iSurveyID ID of the Survey to list participants
     * @param int $iStart Start id of the token list
     * @param int  $iLimit Number of participants to return
     * @param bool $bUnused If you want unused tokens, set true
     * @param bool|array $aAttributes The extented attributes that we want
     * @param array $aConditions Optional conditions to limit the list, either as a
     *              key => value if key is an integer : value is used as comparaison string : sample ['tid = 2']
     *              key=>value search value in column key  : sample ['tid' => '2']
     *              key=>array(operator,value[,value[...]]) using an operator : sample ['tid'=>['=','2']]
     *                  Valid operators are  ['<', '>', '>=', '<=', '=', '<>', 'LIKE', 'IN']
     *                  Only the IN operator allows for several values.
     *              All conditions are connected by AND.
     * @return array The list of tokens
     */
    public function list_participants($sSessionKey, $iSurveyID, $iStart = 0, $iLimit = 10, $bUnused = false, $aAttributes = false, $aConditions = array())
    {
        if ($this->_checkSessionKey($sSessionKey)) {
            $iSurveyID = (int) $iSurveyID;
            $iStart = (int) $iStart;
            $iLimit = (int) $iLimit;
            $oSurvey = Survey::model()->findByPk($iSurveyID);
            if (!isset($oSurvey)) {
                return array('status' => 'Error: Invalid survey ID');
            }

            if (Permission::model()->hasSurveyPermission($iSurveyID, 'tokens', 'read')) {
                if (!tableExists("{{tokens_$iSurveyID}}")) {
                    return array('status' => 'Error: No survey participant list');
                }

                /** @var CDbCriteria mixed> Criteria used in final query below. */
                $oCriteria = new LSDbCriteria();
                $oCriteria->order = 'tid';
                $oCriteria->limit = $iLimit;
                $oCriteria->compare('tid', '>=' . $iStart);
                $addConditionError = $this->addConditionsToCriteria(Token::model($iSurveyID), $oCriteria, $aConditions);
                if (is_string($addConditionError)) {
                    return array('status' => $addConditionError);
                }
                if ($bUnused) {
                    $oTokens = Token::model($iSurveyID)->incomplete()->findAll($oCriteria);
                } else {
                    $oTokens = Token::model($iSurveyID)->findAll($oCriteria);
                }

                if (count($oTokens) == 0) {
                    return array('status' => 'No survey participants found.');
                }

                $extendedAttributes = array();
                if ($aAttributes) {
                    $aBasicDestinationFields = Token::model($iSurveyID)->tableSchema->columnNames;
                    $aTokenProperties = array_intersect($aAttributes, $aBasicDestinationFields);
                    $currentAttributes = array('tid', 'token', 'firstname', 'lastname', 'email');
                    $extendedAttributes = array_diff($aTokenProperties, $currentAttributes);
                }

                foreach ($oTokens as $token) {
                    $token->decrypt();
                    $aTempData = array(
                        'tid' => $token->primarykey,
                        'token' => $token->attributes['token'],
                        'participant_info' => array(
                            'firstname' => $token->attributes['firstname'],
                            'lastname' => $token->attributes['lastname'],
                            'email' => $token->attributes['email'],
                        )
                    );
                    foreach ($extendedAttributes as $sAttribute) {
                        $aTempData[$sAttribute] = $token->attributes[$sAttribute];
                    }
                    $aData[] = $aTempData;
                }
                return $aData;
            } else {
                return array('status' => 'No permission');
            }
        } else {
            return array('status' => self::INVALID_SESSION_KEY);
        }
    }

    /**
     * Return the ids and info of (sub-)questions of a survey/group (RPC function)
     *
     * Returns array of ids and info.
     *
     * @access public
     * @param string $sSessionKey Auth credentials
     * @param int $iSurveyID ID of the Survey to list questions
     * @param int $iGroupID Optional id of the group to list questions
     * @param string $sLanguage Optional parameter language for multilingual questions
     * @return array The list of questions
     */
    public function list_questions($sSessionKey, $iSurveyID, $iGroupID = null, $sLanguage = null)
    {
        if ($this->_checkSessionKey($sSessionKey)) {
            Yii::app()->loadHelper('surveytranslator');
            $iSurveyID = (int) $iSurveyID;
            $oSurvey = Survey::model()->findByPk($iSurveyID);

            if (empty($oSurvey)) {
                return ['status' => 'Error: Invalid survey ID'];
            }

            if (Permission::model()->hasSurveyPermission($iSurveyID, 'surveycontent', 'read')) {
                if (is_null($sLanguage)) {
                    $sLanguage = $oSurvey->language;
                }

                if (!array_key_exists($sLanguage, getLanguageDataRestricted()) or !in_array($sLanguage, $oSurvey->allLanguages)) {
                    return ['status' => 'Error: Invalid language'];
                }

                if ($iGroupID != null) {
                    $iGroupID = (int) $iGroupID;
                    $oGroup = QuestionGroup::model()->findByPk($iGroupID);

                    if (empty($oGroup)) {
                        return ['status' => 'Error: group not found'];
                    }

                    if ($oGroup->sid != $oSurvey->sid) {
                        return ['status' => 'Error: Mismatch in surveyid and groupid'];
                    } else {
                        $aQuestionList = $oGroup->allQuestions;
                    }
                } else {
                    $aQuestionList = $oSurvey->allQuestions;
                }

                if (count($aQuestionList) == 0) {
                    return ['status' => 'No questions found'];
                }

                foreach ($aQuestionList as $oQuestion) {
                    $L10ns = $oQuestion->questionl10ns[$sLanguage];
                    $aData[] = array_merge(
                        [
                            'id' => $oQuestion->primaryKey,
                            'question' => $L10ns->question,
                            'help' => $L10ns->help,
                            'language' => $sLanguage,
                        ],
                        $oQuestion->attributes
                    );
                }
                return $aData;
            } else {
                return ['status' => 'No permission'];
            }
        } else {
            return ['status' => self::INVALID_SESSION_KEY];
        }
    }

    /* Quota specific functions */

    /**
     * Add a new quota with minimum details
     *
     * This just tries to create an empty quota with the minimal settings.
     *
     * Failure status: Invalid session key, No permission, Faulty parameters, Creation Failed result
     *
     * @access public
     * @param string $sSessionKey Auth credentials
     * @param int $iSurveyID ID the Quota will belong to
     * @param string $sQuotaName The name of the new Quota
     * @param int $iLimit Quota limit
     * @param bool $bActive Whether quota is active
     * @param string $sAction ('terminate', 'confirm_terminate')
     * @param bool $bAutoloadURL Whether URL is automatically redirected if quota is triggered
     * @param string $sMessage Message to be presented to the user
     * @param string $sURL URL to be redirected to after finishing the quota
     * @param string $sURLDescription Description of the URL
     * @return array|int The id of the new quota - Or status
     */
    public function add_quota($sSessionKey, $iSurveyID, $sQuotaName, $iLimit, $bActive = true, $sAction = 'terminate', $bAutoloadURL = false, $sMessage = '', $sURL = '', $sURLDescription = '')
    {
        if ($this->_checkSessionKey($sSessionKey)) {
            $iSurveyID = (int) $iSurveyID;
            $iLimit = (int) $iLimit;
            $bActive = (bool) $bActive;
            $sAction = (string) $sAction;
            $bAutoloadURL = (int) $bAutoloadURL;
            $sMessage = (string) $sMessage;
            $sURL = (string) $sURL;
            $sURLDescription = (string) $sURLDescription;

            if (Permission::model()->hasSurveyPermission($iSurveyID, 'quotas', 'create')) {
                $oSurvey = Survey::model()->findByPk($iSurveyID);
                if (!isset($oSurvey)) {
                    return array('status' => 'Error: Invalid survey ID');
                }

                if ($iLimit < 0) {
                    return array('status' => 'Error: Invalid limit');
                }

                switch ($sAction) {
                    case 'terminate':
                        $iAction = Quota::TERMINATE_VISIBLE_QUOTA_QUESTIONS;
                        break;
                    case 'terminate_visible_hidden':
                        $iAction = Quota::TERMINATE_VISIBLE_AND_HIDDEN_QUOTA_QUESTIONS;
                        break;
                    case 'terminate_pages':
                        $iAction = Quota::TERMINATE_ALL_PAGES;
                        break;
                    case 'confirm_terminate':
                        $iAction = Quota::SOFT_TERMINATE_VISIBLE_QUOTA_QUESTIONS;
                        break;
                    default:
                        return array('status' => 'Error: Invalid quota action');
                }

                if ($sMessage == '' && ($sURL != '' || $sURLDescription != '')) {
                    return array('status' => 'Language-specific URL/description is set but no message is given');
                }

                $oDB = Yii::app()->db;
                $oTransaction = $oDB->beginTransaction();

                $oQuota = new Quota();
                $oQuota->sid = $iSurveyID;
                $oQuota->name = $sQuotaName;
                $oQuota->qlimit = $iLimit;
                $oQuota->action = $iAction;
                $oQuota->active = (int) $bActive;
                $oQuota->autoload_url = (int) $bAutoloadURL;

                if (!$oQuota->save()) {
                    return array('status' => 'Creation Failed');
                }

                if (!$sMessage == '') {
                    $oQuotaLanguageSetting = new QuotaLanguageSetting();
                    $oQuotaLanguageSetting->quotals_quota_id = $oQuota->id;
                    $oQuotaLanguageSetting->quotals_language = $oSurvey->language;
                    $oQuotaLanguageSetting->quotals_name = $sQuotaName;
                    $oQuotaLanguageSetting->quotals_message = $sMessage;
                    $oQuotaLanguageSetting->quotals_url = $sURL;
                    $oQuotaLanguageSetting->quotals_urldescrip = $sURLDescription;

                    if (!$oQuotaLanguageSetting->save()) {
                        $oTransaction->rollback();
                        return array('status' => 'Creation Failed');
                    }
                }

                $oTransaction->commit();
                return (int) $oQuota->id;
            } else {
                return array('status' => 'No permission');
            }
        } else {
            return array('status' => 'Invalid session key');
        }
    }

    /**
     * List the quotas in a survey
     *
     * @access public
     * @param string $sSessionKey Auth credentials
     * @param int $iSurveyID ID of the Survey containing the quotas
     * @return array The list of quotas
     */
    public function list_quotas($sSessionKey, $iSurveyID)
    {
        if ($this->_checkSessionKey($sSessionKey)) {
            $iSurveyID = (int) $iSurveyID;
            $oSurvey = Survey::model()->findByPk($iSurveyID);
            if (!isset($oSurvey)) {
                return array('status' => 'Error: Invalid survey ID');
            }

            if (Permission::model()->hasSurveyPermission($iSurveyID, 'quotas', 'read')) {
                $aQuotas = Quota::model()->findAllByAttributes(array('sid' => $iSurveyID));
                if (count($aQuotas) == 0) {
                    return array('status' => 'No quotas found');
                }

                $aData = array();
                foreach ($aQuotas as $oRow) {
                    $aData[] = array(
                        'id' => $oRow->id,
                        'name' => $oRow->name,
                        'action' => $oRow->action,
                        'limit' => $oRow->qlimit,
                        'active' => $oRow->active,
                        'autoload_url' => $oRow->autoload_url,
                    );
                }
                return $aData;
            } else {
                return array('status' => 'No permission');
            }
        } else {
            return array('status' => 'Invalid session key');
        }
    }

    /**
     * Delete a quota
     *
     * @access public
     * @param string $sSessionKey Auth credentials
     * @param int $iQuotaID The ID of the quota to be deleted
     * @return array|int The ID of the deleted quota or status
     */
    public function delete_quota($sSessionKey, $iQuotaID)
    {
        if ($this->_checkSessionKey($sSessionKey)) {
            $iQuotaID = (int) $iQuotaID;
            $oQuota = Quota::model()->findByPk($iQuotaID);
            if (!isset($oQuota)) {
                return array('status' => 'Error: Invalid quota ID');
            }

            if (Permission::model()->hasSurveyPermission($oQuota->sid, 'quotas', 'delete')) {
                $oQuota->deleteQuota(array('id' => $iQuotaID));
                return array('status' => 'OK');
            } else {
                return array('status' => 'No permission');
            }
        } else {
            return array('status' => 'Invalid session key');
        }
    }

    /**
     * Get quota attributes (RPC function)
     *
     * Get properties of a quota
     * All internal properties and completeCount of a quota are available.
     * @see \Quota for the list of available properties
     *
     * Failure status : Invalid quota ID, Invalid session key, No permission, No valid Data
     *
     * @access public
     * @param string $sSessionKey Auth credentials
     * @param integer $iQuotaId Quota ID
     * @param array|null $aQuotaSettings (optional) The properties to get
     * @param string $sLanguage Optional parameter language for multilingual quotas
     * @return array
     */
    public function get_quota_properties($sSessionKey, $iQuotaId, $aQuotaSettings = null, $sLanguage = null)
    {
        if ($this->_checkSessionKey($sSessionKey)) {
            $iQuotaId = (int) $iQuotaId;
            $oQuota = Quota::model()->findByPk($iQuotaId);

            if (!isset($oQuota)) {
                return array('status' => 'Error: Invalid quota ID');
            }

            if (Permission::model()->hasSurveyPermission($oQuota->sid, 'quotas', 'read')) {
                $iSurveyID = $oQuota->sid;
                if (is_null($sLanguage)) {
                    $sLanguage = Survey::model()->findByPk($iSurveyID)->language;
                }

                $aBasicDestinationFields = Quota::model()->tableSchema->columnNames;

                // Quota Language settings
                array_push($aBasicDestinationFields, 'quotals_message');
                array_push($aBasicDestinationFields, 'quotals_url');
                array_push($aBasicDestinationFields, 'quotals_urldescrip');

                // completeCount
                array_push($aBasicDestinationFields, 'completeCount');

                if (!empty($aQuotaSettings)) {
                    $aQuotaSettings = array_intersect($aQuotaSettings, $aBasicDestinationFields);
                } else {
                    $aQuotaSettings = $aBasicDestinationFields;
                }

                if (empty($aQuotaSettings)) {
                    return array('status' => 'No valid Data');
                }

                $aResult = array();
                foreach ($aQuotaSettings as $sPropertyName) {
                    if (isset($oQuota->$sPropertyName)) {
                        $aResult[$sPropertyName] = $oQuota->$sPropertyName;
                    } elseif (
                        isset($oQuota->languagesettings[$sLanguage]) &&
                        isset($oQuota->languagesettings[$sLanguage]->$sPropertyName)
                    ) {
                        $aResult[$sPropertyName] = $oQuota->languagesettings[$sLanguage]->$sPropertyName;
                    }
                }
                return $aResult;
            } else {
                return array('status' => 'No permission');
            }
        } else {
            return array('status' => self::INVALID_SESSION_KEY);
        }
    }

    /**
     * Set quota attributes (RPC function)
     *
     * Retuns an array containing the boolean 'success' and 'message' with either errors or Quota attributes (on success)
     * @access public
     * @param string $sSessionKey Auth credentials
     * @param integer $iQuotaId Quota ID
     * @param array $aQuotaData Quota attributes as array eg ['active'=>1,'limit'=>100]
     * @return array ['success'=>bool, 'message'=>string]
     */
    public function set_quota_properties($sSessionKey, $iQuotaId, $aQuotaData)
    {
        if ($this->_checkSessionKey($sSessionKey)) {
            /** @var Quota $oQuota */
            $iQuotaId = (int) $iQuotaId;
            $oQuota = Quota::model()->findByPk($iQuotaId);
            if (!$oQuota) {
                return [
                    'success' => false,
                    'message' => 'Error: Invalid quota ID'
                ];
            }
            $oSurvey = $oQuota->survey;
            if (Permission::model()->hasSurveyPermission($oSurvey->sid, 'quotas', 'update')) {
                // don't accept id & sid
                if (isset($aQuotaData['id'])) {
                    unset($aQuotaData['id']);
                };
                if (isset($aQuotaData['sid'])) {
                    unset($aQuotaData['sid']);
                };

                // accept boolean input also
                isset($aQuotaData['active']) ? $aQuotaData['active'] = (int) $aQuotaData['active'] : null;
                isset($aQuotaData['autoload_url']) ? $aQuotaData['autoload_url'] = (int) $aQuotaData['autoload_url'] : null;

                $oQuota->attributes = $aQuotaData;
                if (!$oQuota->save()) {
                    return ['success' => false, 'message' => $oQuota->errors];
                } else {
                    return ['success' => true, 'message' => $oQuota->attributes];
                }
            } else {
                return ['success' => false, 'message' => 'Denied!'];
            }
        } else {
            return ['success' => false, 'message' => self::INVALID_SESSION_KEY];
        }
    }

    /**
     * List the survey belonging to a user (RPC function)
     *
     * If user is admin he can get surveys of every user (parameter sUser) or all surveys (sUser=null)
     * Else only the surveys belonging to the user requesting will be shown.
     *
     * Returns array with
     * * `sid` the ids of survey
     * * `gsid` the surey group ids of survey
     * * `surveyls_title` the title of the survey
     * * `startdate` start date
     * * `expires` expiration date
     * * `active` if survey is active (Y) or not (!Y)
     *
     * @access public
     * @param string $sSessionKey Auth credentials
     * @param string|null $sUsername (optional) username to get list of surveys
     * @param integer|null $gsid  (optional) the surveys group id to get list of surveys
     * @return array In case of success the list of surveys
     */
    public function list_surveys($sSessionKey, $sUsername = null, $gsid = null)
    {
        if ($this->_checkSessionKey($sSessionKey)) {
            $oSurvey = new Survey();
            $oSurvey->permission(Yii::app()->user->getId());
            if ($sUsername != null) {
                $aUserData = User::model()->findByAttributes(array('users_name' => (string) $sUsername));
                if (!isset($aUserData)) {
                    return array('status' => 'Invalid user');
                } else {
                    $sUid = $aUserData->attributes['uid'];
                }
                $oSurvey->permission($sUid);
            }
            if (!empty($gsid) && intval($gsid)) {
                $aUserSurveys = $oSurvey->with(
                    array(
                        'languagesettings' => array('condition' => 'surveyls_language=language'),
                        'owner'
                    )
                )->findAll('t.gsid = :gsid', [':gsid' => intval($gsid)]);
            } else {
                $aUserSurveys = $oSurvey->with(array('languagesettings' => array('condition' => 'surveyls_language=language'), 'owner'))->findAll();
            }
            if (count($aUserSurveys) == 0) {
                return array('status' => 'No surveys found');
            }

            foreach ($aUserSurveys as $oSurvey) {
                $oSurveyLanguageSettings = SurveyLanguageSetting::model()->findByAttributes(array('surveyls_survey_id' => $oSurvey->primaryKey, 'surveyls_language' => $oSurvey->language));
                if (!isset($oSurveyLanguageSettings)) {
                    $aSurveyTitle = '';
                } else {
                    $aSurveyTitle = $oSurveyLanguageSettings->attributes['surveyls_title'];
                }
                $aData[] = array(
                    'sid' => $oSurvey->primaryKey,
                    'gsid' => $oSurvey->gsid,
                    'surveyls_title' => $aSurveyTitle,
                    'startdate' => $oSurvey->attributes['startdate'],
                    'expires' => $oSurvey->attributes['expires'],
                    'active' => $oSurvey->attributes['active']
                );
            }
            return $aData;
        } else {
            return array('status' => self::INVALID_SESSION_KEY);
        }
    }

    /**
     * List the survey groups belonging to a user
     *
     * If user is admin he can get survey groups of every user (parameter sUser) or all survey groups (sUser=null)
     * Else only the survey groups belonging to the user requesting will be shown.
     *
     * Returns array with survey group attributes
     *
     * @access public
     * @param string $sSessionKey Auth credentials
     * @param string|null $sUsername (optional) username to get list of survey groups
     * @return array In case of success the list of survey groups
     */
    public function list_survey_groups($sSessionKey, $sUsername = null)
    {
        if ($this->_checkSessionKey($sSessionKey)) {
            $oSurveyGroup = new SurveysGroups();
            if (!Permission::model()->hasGlobalPermission('superadmin', 'read')) {
                $sOwner = Yii::app()->user->getId();
            } elseif ($sUsername != null) {
                $aUserData = User::model()->findByAttributes(array('users_name' => (string) $sUsername));
                if (!isset($aUserData)) {
                    return array('status' => 'Invalid user');
                } else {
                    $sOwner = $aUserData->attributes['uid'];
                }
            }

            if (empty($sOwner)) {
                $aUserSurveyGroups = $oSurveyGroup->findAll();
            } else {
                $aUserSurveyGroups = $oSurveyGroup->findAllByAttributes(array('owner_id' => $sOwner));
            }
            if (count($aUserSurveyGroups) == 0) {
                return array('status' => 'No survey groups found');
            }

            foreach ($aUserSurveyGroups as $oSurveyGroup) {
                $aData[] = $oSurveyGroup->attributes;
            }
            return $aData;
        } else {
            return array('status' => self::INVALID_SESSION_KEY);
        }
    }

    /**
     * Get list the ids and info of administration user(s) (RPC function)
     *
     * Returns array of ids and info.
     *
     * Failure status : Invalid user id, Invalid username, No users found, Invalid session key, Permission denied (super admin is required)
     *
     * @param string $sSessionKey Auth credentials
     * @param int $uid Optional; ID of the user
     * @param string $username Optional; name of the user
     * @return array The list of users in case of success
     */
    public function list_users($sSessionKey = null, $uid = null, $username = null)
    {
        if ($this->_checkSessionKey($sSessionKey)) {
            if (Permission::model()->hasGlobalPermission('superadmin', 'read')) {
                $users = null;
                if ($uid) {
                    $uid = (int) $uid;
                    $user = User::model()->findByPk($uid);
                    if (!$user) {
                        return array('status' => 'Invalid user ID');
                    }
                    $users = array($user);
                } elseif ($username) {
                    $user = User::model()->findByUsername($username);
                    if (!$user) {
                        return array('status' => 'Invalid username');
                    }
                    $users = array($user);
                } else {
                    $users = User::model()->findAll();
                }

                if (count($users) == 0) {
                    return array('status' => 'No users found');
                }

                $basePermissions = Permission::model()->getGlobalBasePermissions();
                $availableActions = array('create', 'read', 'update', 'delete', 'import', 'export');

                foreach ($users as $user) {
                    $attributes = $user->attributes;
                    // The "permissions" array will reflect the permissions stored for the user in
                    // the permissions table, including global, survey and template permissions.
                    // The global permissions in particular may not be the effective ones if the user
                    // has roles assigned.
                    $attributes['permissions'] = [];
                    foreach ($user->permissions as $permission) {
                        $attributes['permissions'][] = $permission->attributes;
                    }
                    // Calculate effective global permissions
                    // These permissions may not match the stored permissions if the user has roles assigned.
                    $attributes['effective_global_permissions'] = [];
                    foreach ($basePermissions as $permissionName => $permission) {
                        $attributes['effective_global_permissions'][$permissionName] = [];
                        foreach ($availableActions as $action) {
                            if (isset($permission[$action]) && $permission[$action]) {
                                $attributes['effective_global_permissions'][$permissionName][$action] = Permission::model()->hasGlobalPermission($permissionName, $action, $user->uid);
                            } else {
                                $attributes['effective_global_permissions'][$permissionName][$action] = false;
                            }
                        }
                    }
                    $attributes['roles'] = [];
                    $roles = $user->roles;
                    if (count($roles) > 0) {
                        foreach ($roles as $role) {
                            $attributes['roles'][] = [
                                'role_id' => $role->ptid,
                                'name' => $role->name,
                                'description' => $role->description,
                            ];
                        }
                    }
                    unset($attributes['password']);
                    unset($attributes['one_time_pw']);
                    $data[] = $attributes;
                }
                return $data;
            } else {
                return array('status' => 'Permission denied.');
            }
        } else {
            return array('status' => self::INVALID_SESSION_KEY);
        }
    }

    /**
     * Activate survey participants (RPC function)
     *
     * Initialise the survey participant list of a survey where new participant tokens may be later added.
     *
     * @access public
     * @param string $sSessionKey Auth credentials
     * @param integer $iSurveyID ID of the Survey where a survey participant list will be created for
     * @param array $aAttributeFields  An array of integer describing any additional attribute fields
     * @return array Status=>OK when successful, otherwise the error description
     */
    public function activate_tokens($sSessionKey, $iSurveyID, $aAttributeFields = array())
    {
        if (!$this->_checkSessionKey($sSessionKey)) {
            return array('status' => self::INVALID_SESSION_KEY);
        }
        $iSurveyID = (int) $iSurveyID;
        $oSurvey = Survey::model()->findByPk($iSurveyID);
        if (is_null($oSurvey)) {
            return array('status' => 'Error: Invalid survey ID');
        }
        if (
            /* Same test Tokens->newParticipantTable */
            !Permission::model()->hasSurveyPermission($iSurveyID, 'surveysettings', 'update') &&
            !Permission::model()->hasSurveyPermission($iSurveyID, 'tokens', 'create')
        ) {
            return array('status' => 'No permission');
        }
        if (is_array($aAttributeFields) && count($aAttributeFields) > 0) {
            foreach ($aAttributeFields as &$sField) {
                $sField = intval($sField);
                $sField = 'attribute_' . $sField;
            }
            $aAttributeFields = array_unique($aAttributeFields);
        }
        Yii::app()->loadHelper('admin.token');
        if (Token::createTable($iSurveyID, $aAttributeFields)) {
            return array('status' => 'OK');
        } else {
            return array('status' => 'Survey participant list could not be created');
        }
    }

    /**
     * Send e-mails to registered participants in a survey (RPC function)
     *
     * Returns array of results of sending
     *
     * Default behaviour is to send register emails to not invited, not reminded, not completed and in valid frame date participant.
     *
     * @access public
     * @param string $sSessionKey Auth credentials
     * @param int $iSurveyID ID of the Survey that participants belong
     * @param array $overrideAllConditions replace the default conditions, either as a
     *              key => value if key is an integer : value is used as comparaison string : sample ['tid = 2']
     *              key=>value search value in column key  : sample ['tid' => '2']
     *              key=>array(operator,value[,value[...]]) using an operator : sample ['tid'=>['=','2']]
     *                  Valid operators are  ['<', '>', '>=', '<=', '=', '<>', 'LIKE', 'IN']
     *                  Only the IN operator allows for several values.
     *              All conditions are connected by AND.
     * @return array Result of the action
     */
    public function mail_registered_participants($sSessionKey, $iSurveyID, $overrideAllConditions = array())
    {
        Yii::app()->loadHelper('admin.token');
        if (!$this->_checkSessionKey($sSessionKey)) {
            return array('status' => self::INVALID_SESSION_KEY);
        }
        $iSurveyID = (int) $iSurveyID;
        $oSurvey = Survey::model()->findByPk($iSurveyID);
        if (!isset($oSurvey)) {
            return array('status' => 'Error: Invalid survey ID');
        }

        if (Permission::model()->hasSurveyPermission($iSurveyID, 'tokens', 'update')) {
            if (!tableExists("{{tokens_$iSurveyID}}")) {
                return array('status' => 'Error: No survey participant list');
            }

            $command = new LSDbCriteria();
            if (count($overrideAllConditions)) {
                $addConditionError = $this->addConditionsToCriteria(Token::model($iSurveyID), $command, $overrideAllConditions);
                if (is_string($addConditionError)) {
                    return array('status' => $addConditionError);
                }
            } else {
                $sNow = date('Y-m-d H:i:s', strtotime((string) Yii::app()->getConfig('timeadjust'), strtotime(date('Y-m-d H:i:s'))));
                $command->addCondition('usesleft > 0');
                $command->addCondition("sent = 'N'");
                $command->addCondition("remindersent = 'N'");
                $command->addCondition("(completed ='N') or (completed='')");
                $command->addCondition("validfrom is null OR validfrom < '{$sNow}'");
                $command->addCondition("validuntil is null OR validuntil > '{$sNow}'");
                $command->addCondition("emailstatus = 'OK'");
            }
            $command->order = 'tid';

            $aAllTokens = Token::model($iSurveyID)->findAll($command);
            $iAllTokensCount = count($aAllTokens);
            unset($aAllTokens);

            $iMaxEmails = (int) Yii::app()->getConfig('maxemails');
            $command->limit = $iMaxEmails;
            $aResultTokens = Token::model($iSurveyID)->findAll($command);

            if (empty($aResultTokens)) {
                return array('status' => 'Error: No candidate tokens');
            }

            foreach ($aResultTokens as $key => $oToken) {
                $oToken->decrypt();
                // pattern taken from php_filter_validate_email PHP_5_4/ext/filter/logical_filters.c
                /* @todo : use LimeMailer::validateAddresses */
                $pattern = '/^(?!(?:(?:\x22?\x5C[\x00-\x7E]\x22?)|(?:\x22?[^\x5C\x22]\x22?)){255,})(?!(?:(?:\x22?\x5C[\x00-\x7E]\x22?)|(?:\x22?[^\x5C\x22]\x22?)){65,}@)(?:(?:[\x21\x23-\x27\x2A\x2B\x2D\x2F-\x39\x3D\x3F\x5E-\x7E]+)|(?:\x22(?:[\x01-\x08\x0B\x0C\x0E-\x1F\x21\x23-\x5B\x5D-\x7F]|(?:\x5C[\x00-\x7F]))*\x22))(?:\.(?:(?:[\x21\x23-\x27\x2A\x2B\x2D\x2F-\x39\x3D\x3F\x5E-\x7E]+)|(?:\x22(?:[\x01-\x08\x0B\x0C\x0E-\x1F\x21\x23-\x5B\x5D-\x7F]|(?:\x5C[\x00-\x7F]))*\x22)))*@(?:(?:(?!.*[^.]{64,})(?:(?:(?:xn--)?[a-z0-9]+(?:-+[a-z0-9]+)*\.){1,126}){1,}(?:(?:[a-z][a-z0-9]*)|(?:(?:xn--)[a-z0-9]+))(?:-+[a-z0-9]+)*)|(?:\[(?:(?:IPv6:(?:(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){7})|(?:(?!(?:.*[a-f0-9][:\]]){7,})(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){0,5})?::(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){0,5})?)))|(?:(?:IPv6:(?:(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){5}:)|(?:(?!(?:.*[a-f0-9]:){5,})(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){0,3})?::(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){0,3}:)?)))?(?:(?:25[0-5])|(?:2[0-4][0-9])|(?:1[0-9]{2})|(?:[1-9]?[0-9]))(?:\.(?:(?:25[0-5])|(?:2[0-4][0-9])|(?:1[0-9]{2})|(?:[1-9]?[0-9]))){3}))\]))$/iD';

                // pattern to split in case of multiple emails for a participant
                $emailsToCheck = preg_split('/(,|;)/', (string) $oToken['email']);

                // Loop through each email and validate it
                foreach ($emailsToCheck as $emailToCheck) {
                    if (preg_match($pattern, (string) $emailToCheck) !== 1) {
                        unset($aResultTokens[$key]);
                        // subtract from 'left to send'
                        $iAllTokensCount--;
                        break;
                    }
                }
            }

            if (empty($aResultTokens)) {
                return array('status' => 'Error: No candidate tokens');
            }

            $aResult = emailTokens($iSurveyID, $aResultTokens, 'register');
            $iLeft = $iAllTokensCount - count($aResultTokens);
            $aResult['status'] = $iLeft . ' left to send';

            return $aResult;
        } else {
            return array('status' => 'No permission');
        }
    }

    /**
     * Invite participants in a survey (RPC function)
     *
     * Returns array of results of sending.
     * The sending stops on the first error (ie. when a token is invalid).
     *
     * @access public
     * @param string $sSessionKey Auth credentials
     * @param int $iSurveyID ID of the survey that participants belong
     * @param array $aTokenIds Ids of the participant to invite
     * @param bool $bEmail Send only pending invites (TRUE) or resend invites only (FALSE)
     * @param bool $continueOnError Don't stop on first invalid participant
     * @return array Result of the action
     */
    public function invite_participants($sSessionKey, $iSurveyID, $aTokenIds = null, $bEmail = true, $continueOnError = false)
    {
        Yii::app()->loadHelper('admin.token');
        if (!$this->_checkSessionKey($sSessionKey)) {
            return array('status' => self::INVALID_SESSION_KEY);
        }
        $iSurveyID = (int) $iSurveyID;
        $oSurvey = Survey::model()->findByPk($iSurveyID);
        if (!isset($oSurvey)) {
            return array('status' => 'Error: Invalid survey ID');
        }

        if (Permission::model()->hasSurveyPermission($iSurveyID, 'tokens', 'update')) {
            if (!tableExists("{{tokens_$iSurveyID}}")) {
                return array('status' => 'Error: No survey participant list');
            }

            $iMaxEmails = (int) Yii::app()->getConfig('maxemails');
            $SQLemailstatuscondition = "emailstatus = 'OK'";

            $oTokens = TokenDynamic::model($iSurveyID);
            $aResultTokens = $oTokens->findUninvited($aTokenIds, $iMaxEmails, $bEmail, $SQLemailstatuscondition);
            $aAllTokens = $oTokens->findParticipantIDs(false, 0, true, $SQLemailstatuscondition);
            $iAllTokensCount = count($aAllTokens);
            unset($aAllTokens);
            if (empty($aResultTokens)) {
                return array('status' => 'Error: No candidate tokens');
            }

            foreach ($aResultTokens as $key => $oToken) {
                // pattern taken from php_filter_validate_email PHP_5_4/ext/filter/logical_filters.c
                $pattern = '/^(?!(?:(?:\x22?\x5C[\x00-\x7E]\x22?)|(?:\x22?[^\x5C\x22]\x22?)){255,})(?!(?:(?:\x22?\x5C[\x00-\x7E]\x22?)|(?:\x22?[^\x5C\x22]\x22?)){65,}@)(?:(?:[\x21\x23-\x27\x2A\x2B\x2D\x2F-\x39\x3D\x3F\x5E-\x7E]+)|(?:\x22(?:[\x01-\x08\x0B\x0C\x0E-\x1F\x21\x23-\x5B\x5D-\x7F]|(?:\x5C[\x00-\x7F]))*\x22))(?:\.(?:(?:[\x21\x23-\x27\x2A\x2B\x2D\x2F-\x39\x3D\x3F\x5E-\x7E]+)|(?:\x22(?:[\x01-\x08\x0B\x0C\x0E-\x1F\x21\x23-\x5B\x5D-\x7F]|(?:\x5C[\x00-\x7F]))*\x22)))*@(?:(?:(?!.*[^.]{64,})(?:(?:(?:xn--)?[a-z0-9]+(?:-+[a-z0-9]+)*\.){1,126}){1,}(?:(?:[a-z][a-z0-9]*)|(?:(?:xn--)[a-z0-9]+))(?:-+[a-z0-9]+)*)|(?:\[(?:(?:IPv6:(?:(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){7})|(?:(?!(?:.*[a-f0-9][:\]]){7,})(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){0,5})?::(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){0,5})?)))|(?:(?:IPv6:(?:(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){5}:)|(?:(?!(?:.*[a-f0-9]:){5,})(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){0,3})?::(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){0,3}:)?)))?(?:(?:25[0-5])|(?:2[0-4][0-9])|(?:1[0-9]{2})|(?:[1-9]?[0-9]))(?:\.(?:(?:25[0-5])|(?:2[0-4][0-9])|(?:1[0-9]{2})|(?:[1-9]?[0-9]))){3}))\]))$/iD';

                // pattern to split in case of multiple emails for a participant
                $emailsToCheck = preg_split('/(,|;)/', (string) $oToken['email']);

                // Loop through each email and validate it
                foreach ($emailsToCheck as $emailToCheck) {
                    if (preg_match($pattern, (string) $emailToCheck) !== 1) {
                        unset($aResultTokens[$key]);
                        break;
                    }
                }
            }

            if (empty($aResultTokens)) {
                return array('status' => 'Error: No candidate tokens');
            }
            $aResult = emailTokens($iSurveyID, $aResultTokens, 'invite', $continueOnError);
            $iLeft = $iAllTokensCount - count($aResultTokens);
            $aResult['status'] = $iLeft . ' left to send';

            return $aResult;
        } else {
            return array('status' => 'No permission');
        }
    }

    /**
     * Send a reminder to participants in a survey (RPC function)
     *
     * Returns array of results of sending
     *
     * @access public
     * @param string $sSessionKey Auth credentials
     * @param int $iSurveyID ID of the Survey that participants belong
     * @param int $iMinDaysBetween (optional) parameter days from last reminder
     * @param int $iMaxReminders (optional) parameter Maximum reminders count
     * @param array $aTokenIds Ids of the participant to remind (optional filter)
     * @param bool $continueOnError Don't stop on first invalid participant
     * @return array in case of success array of result of each email send action and count of invitations left to send in status key
     */
    public function remind_participants($sSessionKey, $iSurveyID, $iMinDaysBetween = null, $iMaxReminders = null, $aTokenIds = false, $continueOnError = false)
    {
        Yii::app()->loadHelper('admin.token');
        if (!$this->_checkSessionKey($sSessionKey)) {
            return array('status' => self::INVALID_SESSION_KEY);
        }
        $iSurveyID = (int) $iSurveyID;
        $oSurvey = Survey::model()->findByPk($iSurveyID);
        if (!isset($oSurvey)) {
            return array('status' => 'Error: Invalid survey ID');
        }

        if (Permission::model()->hasSurveyPermission($iSurveyID, 'tokens', 'update')) {
            $timeadjust = Yii::app()->getConfig('timeadjust');

            if (!tableExists("{{tokens_$iSurveyID}}")) {
                return array('status' => 'Error: No survey participant list');
            }

            $SQLemailstatuscondition = "emailstatus = 'OK'";
            $SQLremindercountcondition = '';
            $SQLreminderdelaycondition = '';
            $iMaxEmails = (int) Yii::app()->getConfig('maxemails');

            if (!is_null($iMinDaysBetween)) {
                $iMinDaysBetween = (int) $iMinDaysBetween;
                $compareddate = dateShift(date('Y-m-d H:i:s', time() - 86400 * $iMinDaysBetween), 'Y-m-d H:i', $timeadjust);
                $SQLreminderdelaycondition = " ((remindersent = 'N' AND sent < '" . $compareddate . "')  OR  (remindersent < '" . $compareddate . "'))";
            }

            if (!is_null($iMaxReminders)) {
                $iMaxReminders = (int) $iMaxReminders;
                $SQLremindercountcondition = 'remindercount < ' . $iMaxReminders;
            }

            $oTokens = TokenDynamic::model($iSurveyID);
            $aAllTokens = $oTokens->findParticipantIDs(false, 0, false, $SQLemailstatuscondition, $SQLremindercountcondition, $SQLreminderdelaycondition);
            $iAllTokensCount = count($aAllTokens);
            unset($aAllTokens);  // save some memory before the next query

            $aResultTokens = $oTokens->findUninvited($aTokenIds, $iMaxEmails, false, $SQLemailstatuscondition, $SQLremindercountcondition, $SQLreminderdelaycondition);

            if (empty($aResultTokens)) {
                return array('status' => 'Error: No candidate tokens');
            }

            $aResult = emailTokens($iSurveyID, $aResultTokens, 'remind', $continueOnError);

            $iLeft = $iAllTokensCount - count($aResultTokens);
            $aResult['status'] = $iLeft . ' left to send';
            return $aResult;
        } else {
            return array('status' => 'No permission');
        }
    }

    /* Response specific functions */

    /**
     * Add a response to the survey responses collection.
     *
     * Returns the id of the inserted survey response
     *
     * @access public
     * @param string $sSessionKey Auth credentials
     * @param int $iSurveyID ID of the Survey to insert responses
     * @param array $aResponseData The actual response
     * @return int|array The response ID or an array with status message (can include result_id)
     * @todo Need to clean up return array, especially the case when response was added but file not uploaded.
     * @todo See discussion: https://bugs.limesurvey.org/view.php?id=13794
     */
    public function add_response($sSessionKey, $iSurveyID, $aResponseData)
    {
        if (!$this->_checkSessionKey($sSessionKey)) {
            return array('status' => self::INVALID_SESSION_KEY);
        }
        $iSurveyID = (int) $iSurveyID;
        $oSurvey = Survey::model()->findByPk($iSurveyID);

        if (is_null($oSurvey)) {
            return array('status' => 'Error: Invalid survey ID');
        }

        if (Permission::model()->hasSurveyPermission($iSurveyID, 'responses', 'create')) {
            if (!Yii::app()->db->schema->getTable($oSurvey->responsesTableName)) {
                return array('status' => 'No survey response table');
            }

            // set required values if not set

            // @todo: Some of this is part of the validation and should be done in the model instead
            if (array_key_exists('submitdate', $aResponseData) && empty($aResponseData['submitdate'])) {
                unset($aResponseData['submitdate']);
            } elseif (!isset($aResponseData['submitdate'])) {
                $aResponseData['submitdate'] = date('Y-m-d H:i:s');
            }
            if (!isset($aResponseData['startlanguage'])) {
                $aResponseData['startlanguage'] = $oSurvey->language;
            }

            if ($oSurvey->isDateStamp) {
                if (array_key_exists('datestamp', $aResponseData) && empty($aResponseData['datestamp'])) {
                    unset($aResponseData['datestamp']);
                } elseif (!isset($aResponseData['datestamp'])) {
                    $aResponseData['datestamp'] = date('Y-m-d H:i:s');
                }
                if (array_key_exists('startdate', $aResponseData) && empty($aResponseData['startdate'])) {
                    unset($aResponseData['startdate']);
                } elseif (!isset($aResponseData['startdate'])) {
                    $aResponseData['startdate'] = date('Y-m-d H:i:s');
                }
            }

            SurveyDynamic::sid($iSurveyID);
            $survey_dynamic = new SurveyDynamic();
            $aBasicDestinationFields = $survey_dynamic->tableSchema->columnNames;
            $aResponseData = array_intersect_key($aResponseData, array_flip($aBasicDestinationFields));
            $survey_dynamic->setAttributes($aResponseData, false);
            $survey_dynamic->encryptSave();

            if ($survey_dynamic->id) {
                $result_id = $survey_dynamic->id;
                $oResponse = Response::model($iSurveyID)->findByAttributes(array('id' => $result_id))->decrypt();
                foreach ($oResponse->getFiles() as $aFile) {
                    $sUploadPath = Yii::app()->getConfig('uploaddir') . '/surveys/' . $iSurveyID . '/files/';
                    $sFileRealName = Yii::app()->getConfig('uploaddir') . '/surveys/' . $iSurveyID . '/files/' . $aFile['filename'];
                    $sFileTempName = Yii::app()->getConfig('tempdir') . '/upload/' . $aFile['filename'];

                    if (!file_exists($sFileRealName)) {
                        if (!is_dir($sUploadPath)) {
                            mkdir($sUploadPath, 0777, true);
                        }

                        if (!rename($sFileTempName, $sFileRealName)) {
                            return array(
                                'status' => 'Unable to move files ' . $sFileTempName . ' ' . $sFileRealName,
                                'result_id' => $result_id
                            );
                        }
                    }
                }

                return $result_id;
            } else {
                return array('status' => 'Unable to add response');
            }
        } else {
            return array('status' => 'No permission');
        }
    }

    /**
     * Update a response in a given survey.
     *
     * Routine supports only single response updates.
     * Response to update will be identified either by the response id, or the token if response id is missing.
     * Routine is only applicable for active surveys with alloweditaftercompletion = Y.
     *
     * @access public
     * @param string $sSessionKey Auth credentials
     * @param int $iSurveyID Id of the Survey to update response
     * @param array $aResponseData The actual response
     * @return array|boolean TRUE(bool) on success. Array with error status on failure.
     */
    public function update_response($sSessionKey, $iSurveyID, $aResponseData)
    {
        if (!$this->_checkSessionKey($sSessionKey)) {
            return array('status' => self::INVALID_SESSION_KEY);
        }
        $iSurveyID = (int) $iSurveyID;
        $oSurvey = Survey::model()->findByPk($iSurveyID);
        if (is_null($oSurvey)) {
            return array('status' => 'Invalid survey ID');
        }
        if (!$oSurvey->isActive) {
            return array('status' => 'Survey is not active');
        }

        if (!$oSurvey->isAllowEditAfterCompletion) {
            return array('status' => 'Survey does not allow edit after completion');
        }

        if (Permission::model()->hasSurveyPermission($iSurveyID, 'responses', 'update')) {
            if (!Yii::app()->db->schema->getTable($oSurvey->responsesTableName)) {
                return array('status' => 'No survey response table');
            }

            if (
                !isset($aResponseData['id']) &&
                !isset($aResponseData['token'])
            ) {
                return array('status' => 'Missing response identifier (id|token)');
            }

            SurveyDynamic::sid($iSurveyID);
            $oSurveyDynamic = new SurveyDynamic();

            if (isset($aResponseData['id'])) {
                $aResponses = $oSurveyDynamic->findAllByPk((int) $aResponseData['id']);
            } else {
                $aResponses = $oSurveyDynamic->findAllByAttributes(array('token' => $aResponseData['token']));
            }

            if (empty($aResponses)) {
                return array('status' => 'No matching Response');
            }
            if (count($aResponses) > 1) {
                return array('status' => 'More then one matching response, updateing multiple responses at once is not supported');
            }

            $aBasicDestinationFields = $oSurveyDynamic->tableSchema->columnNames;
            $aInvalidFields = array_diff_key($aResponseData, array_flip($aBasicDestinationFields));
            if (count($aInvalidFields) > 0) {
                return array('status' => 'Invalid Column names supplied: ' . implode(', ', array_keys($aInvalidFields)));
            }

            unset($aResponseData['token']);

            foreach ($aResponseData as $sAtributeName => $value) {
                $aResponses[0]->setAttribute($sAtributeName, $value);
            }

            $bResult = $aResponses[0]->encryptSave(true);

            if ($bResult) {
                return $bResult;
            } else {
                return array('status' => 'Unable to edit response');
            }
        } else {
            return array('status' => 'No permission');
        }
    }

    /**
     * Delete a response in a given survey using its Id
     *
     * RPC Routine to delete responses of particular id in a survey.
     * Returns array
     *
     * @access public
     * @param string $sSessionKey Auth credentials
     * @param int $iSurveyID Id of the survey that participants belong
     * @param int $iResponseID Id of the response to delete
     * @return array Result of the change action
     */
    public function delete_response($sSessionKey, $iSurveyID, $iResponseID)
    {
        // check sessionKey is valid or not
        if ($this->_checkSessionKey($sSessionKey)) {
            $oSurvey = Survey::model()->findByPk($iSurveyID);
            if (!isset($oSurvey)) {
                return array('status' => 'Error: Invalid survey ID');
            }

            if (Permission::model()->hasSurveyPermission($iSurveyID, 'responses', 'delete')) {
                // get response id from response table using ID
                $Response = Response::model($iSurveyID)->findByPk($iResponseID);
                if ($Response) {
                    // delete the files and timings and row
                    if ($Response->delete()) {
                        return array($iResponseID => 'deleted');
                    }
                    return array('status' => 'Response not deleted for unknow reason');
                } else {
                    return array('status' => 'Response Id not found');
                }
            } else {
                return array('status' => 'No permission');
            }
        } else {
            return array('status' => self::INVALID_SESSION_KEY);
        }
    }

    /**
     * Uploads one file to be used later.
     *
     * Returns the metadata on success.
     *
     * @access public
     * @param string $sSessionKey Auth credentials
     * @param int $iSurveyID ID of the Survey to insert file
     * @param string $sFieldName the Field to upload file
     * @param string $sFileName the uploaded file name
     * @param string $sFileContent the uploaded file content encoded as BASE64
     * @return array The file metadata with final upload path or error description
     */
    public function upload_file($sSessionKey, $iSurveyID, $sFieldName, $sFileName, $sFileContent)
    {
        if (!$this->_checkSessionKey($sSessionKey)) {
            return array('status' => self::INVALID_SESSION_KEY);
        }
        $iSurveyID = (int) $iSurveyID;
        $oSurvey = Survey::model()->findByPk($iSurveyID);

        if (is_null($oSurvey)) {
            return array('status' => 'Error: Invalid survey ID');
        }

        if (Permission::model()->hasSurveyPermission($iSurveyID, 'responses', 'create')) {
            if (!Yii::app()->db->schema->getTable('{{survey_' . $iSurveyID . '}}')) {
                return array('status' => 'No survey response table');
            }
        } else {
            return array('status' => 'No permission');
        }

        $sUploadDir = Yii::app()->getConfig('uploaddir') . '/surveys/' . $iSurveyID . '/files/';

        if (!file_exists($sUploadDir)) {
            if (!mkdir($sUploadDir)) {
                return array('status' => 'Can not make upload directory');
            }
        }

        $aFieldMap = createFieldMap($oSurvey, 'short', false, false, Yii::app()->getConfig('defaultlang'));
        if (!isset($aFieldMap[$sFieldName])) {
            return array('status' => 'Can not obtain field map');
        }

        $aAttributes = QuestionAttribute::model()->getQuestionAttributes($aFieldMap[$sFieldName]['qid']);

        $iFileUploadTotalSpaceMB = Yii::app()->getConfig('iFileUploadTotalSpaceMB');

        $maxfilesize = (int) $aAttributes['max_filesize'];
        $allowed_filetypes = $aAttributes['allowed_filetypes'];
        $valid_extensions_array = explode(',', (string) $allowed_filetypes);
        $valid_extensions_array = array_map('trim', $valid_extensions_array);

        $pathinfo = pathinfo($sFileName);
        $ext = strtolower($pathinfo['extension']);

        // check to see that this file type is allowed
        if (!in_array($ext, $valid_extensions_array)) {
            return array('status' => 'The extension ' . $ext . ' is not valid. Valid extensions are: ' . $allowed_filetypes);
        }

        // This also accounts for BASE64 overhead
        $size = (0.001 * 3 * strlen($sFileContent)) / 4;

        $randfilename = 'fu_' . randomChars(15) . '_' . $pathinfo['extension'];
        $randfileloc = $sUploadDir . $randfilename;

        if ($size > $maxfilesize) {
            return array('status' => sprintf('Sorry, this file is too large. Only files up to %s KB are allowed.', $maxfilesize));
        }

        if ($iFileUploadTotalSpaceMB > 0 && ((calculateTotalFileUploadUsage() + ($size / 1024 / 1024)) > $iFileUploadTotalSpaceMB)) {
            return array('status' => 'Not enough free space available');
        }

        $uploaded = file_put_contents($randfileloc, base64_decode($sFileContent));
        if ($uploaded === false) {
            return array('status' => 'Unable to write file');
        }

        return array(
            'success' => true,
            'size' => $size,
            'name' => rawurlencode(basename($sFileName)),
            'ext' => $ext,
            'filename' => $randfilename,
            'msg' => gT('The file has been successfully uploaded.')
        );
    }

    /**
     * Export responses in base64 encoded string
     *
     * @access public
     * @param string $sSessionKey Auth credentials
     * @param int $iSurveyID ID of the Survey
     * @param string $sDocumentType any format available by plugins (for example : pdf, csv, xls, doc, json)
     * @param string $sLanguageCode (optional) The language to be used
     * @param string $sCompletionStatus (optional) 'complete','incomplete' or 'all' - defaults to 'all'
     * @param string $sHeadingType (optional) 'code','full' or 'abbreviated' Optional defaults to 'code'
     * @param string $sResponseType (optional)'short' or 'long' Optional defaults to 'short'
     * @param integer $iFromResponseID (optional) Frpm response id
     * @param integer $iToResponseID (optional) To response id
     * @param array $aFields (optional) Name the fields to export
     * @param array $aAdditionalOptions (optional) Addition options for export, @see \FormattingOptions, example : 'convertY', 'convertN', 'nValue', 'yValue', 'headerSpacesToUnderscores', 'useEMCode'
     * @return array|string On success: Requested file as base 64-encoded string. On failure array with error information
     */
    public function export_responses($sSessionKey, $iSurveyID, $sDocumentType, $sLanguageCode = null, $sCompletionStatus = 'all', $sHeadingType = 'code', $sResponseType = 'short', $iFromResponseID = null, $iToResponseID = null, $aFields = null, $aAdditionalOptions = null)
    {
        $iSurveyID = (int) $iSurveyID;
        $survey = Survey::model()->findByPk($iSurveyID);

        if (!$this->_checkSessionKey($sSessionKey)) {
            return array('status' => self::INVALID_SESSION_KEY);
        }
        if (!Permission::model()->hasSurveyPermission($iSurveyID, 'responses', 'export')) {
            return array('status' => 'No permission');
        }
        Yii::app()->loadHelper('admin.exportresults');
        if (!tableExists($survey->responsesTableName)) {
            return array('status' => 'No Data, survey table does not exist.');
        }
        if (!($maxId = SurveyDynamic::model($iSurveyID)->getMaxId(null, true))) {
            return array('status' => 'No Data, could not get max id.');
        }
        if (!empty($sLanguageCode) && !in_array($sLanguageCode, $survey->getAllLanguages())) {
            return array('status' => 'Language code not found for this survey.');
        }

        if (empty($sLanguageCode)) {
            $sLanguageCode = $survey->language;
        }
        if (is_null($aFields)) {
            $aFields = array_keys(createFieldMap($survey, 'full', true, false, $sLanguageCode));
        }
        if ($sDocumentType == 'xls') {
            // Cut down to the first 255 fields
            $aFields = array_slice($aFields, 0, 255);
        }
        $oFormattingOptions = new FormattingOptions();

        if (is_array($aAdditionalOptions)) {
            foreach ($aAdditionalOptions as $option => $value) {
                if (property_exists($oFormattingOptions, $option)) {
                    $oFormattingOptions->$option = $value;
                }
            }
            /* fix some specific option (GUI use 1 and 2, but default for remote control are set to Y and N before) */
            if (!empty($oFormattingOptions->convertY) && !isset($oFormattingOptions->yValue)) {
                $oFormattingOptions->yValue = 'Y';
            }
            if (!empty($oFormattingOptions->convertN) && !isset($oFormattingOptions->nValue)) {
                $oFormattingOptions->nValue = 'N';
            }
        }

        if ($iFromResponseID != null) {
            $oFormattingOptions->responseMinRecord = (int) $iFromResponseID;
        } else {
            $oFormattingOptions->responseMinRecord = 1;
        }

        if ($iToResponseID != null) {
            $oFormattingOptions->responseMaxRecord = (int) $iToResponseID;
        } else {
            $oFormattingOptions->responseMaxRecord = $maxId;
        }

        $oFormattingOptions->selectedColumns = $aFields;
        $oFormattingOptions->responseCompletionState = $sCompletionStatus;
        $oFormattingOptions->headingFormat = $sHeadingType;
        $oFormattingOptions->answerFormat = $sResponseType;
        $oFormattingOptions->output = 'file';

        $oExport = new ExportSurveyResultsService();
        $sTempFile = $oExport->exportResponses($iSurveyID, $sLanguageCode, $sDocumentType, $oFormattingOptions, '');
        return new BigFile($sTempFile, true, 'base64');
    }

    /**
     * Export token response in a survey.
     *
     * Returns the requested file as base64 encoded string
     *
     * @access public
     * @param string $sSessionKey Auth credentials
     * @param int $iSurveyID ID of the Survey
     * @param string $sDocumentType pdf, csv, xls, doc, json
     * @param array|string $aTokens The tokens (or single token) for which responses needed
     * @param string $sLanguageCode The language to be used
     * @param string $sCompletionStatus Optional 'complete','incomplete' or 'all' - defaults to 'all'
     * @param string $sHeadingType 'code','full' or 'abbreviated' Optional defaults to 'code'
     * @param string $sResponseType 'short' or 'long' Optional defaults to 'short'
     * @param array $aFields Optional Selected fields
     * @return array|string On success: Requested file as base 64-encoded string. On failure array with error information
     */
    public function export_responses_by_token($sSessionKey, $iSurveyID, $sDocumentType, $aTokens, $sLanguageCode = null, $sCompletionStatus = 'all', $sHeadingType = 'code', $sResponseType = 'short', $aFields = null)
    {
        $iSurveyID = (int) $iSurveyID;
        $survey = Survey::model()->findByPk($iSurveyID);
        if (!$this->_checkSessionKey($sSessionKey)) {
            return array('status' => self::INVALID_SESSION_KEY);
        }
        Yii::app()->loadHelper('admin.exportresults');
        if (!tableExists($survey->responsesTableName)) {
            return array('status' => 'No Data, survey table does not exist.');
        }
        if (!($maxId = SurveyDynamic::model($iSurveyID)->getMaxId())) {
            return array('status' => 'No Data, could not get max id.');
        }
        if (!empty($sLanguageCode) && !in_array($sLanguageCode, $survey->getAllLanguages())) {
            return array('status' => 'Language code not found for this survey.');
        }
        if (is_array($aTokens)) {
            if (count($aTokens) == 0) {
                return array('status' => 'No Data, empty tokens array parameter');
            } else {
                $aTokensQuoted = Array();
                foreach ($aTokens as $token) {
                    array_push($aTokensQuoted, App()->db->quoteValue("$token"));
                }
                $tokenFilter = ' IN (' . implode(',', $aTokensQuoted) . ')';
            }
        } else {
            if (!SurveyDynamic::model($iSurveyID)->findByAttributes(array('token' => $aTokens))) {
                return array('status' => 'No Response found for Token');
            }
            $tokenFilter = '=' . App()->db->quoteValue("$aTokens");
        }
        if (!Permission::model()->hasSurveyPermission($iSurveyID, 'responses', 'export')) {
            return array('status' => 'No permission');
        }
        if (empty($sLanguageCode)) {
            $sLanguageCode = $survey->language;
        }
        if (is_null($aFields)) {
            $aFields = array_keys(createFieldMap($survey, 'full', true, false, $sLanguageCode));
        }
        $oFormattingOptions = new FormattingOptions();
        $oFormattingOptions->responseMinRecord = 1;
        $oFormattingOptions->responseMaxRecord = $maxId;
        $oFormattingOptions->selectedColumns = $aFields;
        $oFormattingOptions->responseCompletionState = $sCompletionStatus;
        $oFormattingOptions->headingFormat = $sHeadingType;
        $oFormattingOptions->answerFormat = $sResponseType;
        $oFormattingOptions->output = 'file';
        $oExport = new ExportSurveyResultsService();
        $sTableName = Yii::app()->db->tablePrefix . 'survey_' . $iSurveyID;
        $sTempFile = $oExport->exportResponses($iSurveyID, $sLanguageCode, $sDocumentType, $oFormattingOptions, "{$sTableName}.token" . $tokenFilter);
        return new BigFile($sTempFile, true, 'base64');
    }

    /**
     * Obtain all uploaded files for all responses
     *
     * @access public
     *
     * @param string  $sSessionKey  Auth credentials
     * @param int     $iSurveyID    ID of the Survey
     * @param string  $sToken       Response token
     * @param int     $responseId   Response ID
     *
     * @return array On success: array containing all uploads of the specified response
     *               On failure: array with error information
     */
    public function get_uploaded_files($sSessionKey, $iSurveyID, $sToken, $responseId = null)
    {
        if (!$this->_checkSessionKey($sSessionKey)) {
            return array('status' => self::INVALID_SESSION_KEY);
        }

        $iSurveyID = (int) $iSurveyID;
        $oSurvey = Survey::model()->findByPk($iSurveyID);
        if (!$oSurvey->hasResponsesTable) {
            return array('status' => 'No Data, survey table does not exist.');
        }

        if (!Permission::model()->hasSurveyPermission($iSurveyID, 'responses', 'read')) {
            return array('status' => 'No permission');
        }

        if (empty($responseId) and empty($sToken)) {
            return ['status' => 'Invalid arguments: both Token and Reponse ID are empty'];
        }
        $criteria = new CDbCriteria();
        if (!empty($responseId)) {
            $criteria->compare('id', $responseId);
        }
        if (!empty($sToken)) {
            $criteria->compare('token', $sToken);
        }
        $oResponses = Response::model($iSurveyID)->findAll($criteria);
        if (empty($oResponses)) {
            return ['status' => 'Could not find response for given token or response id'];
        }

        $uploaded_files = array();
        foreach ($oResponses as $key => $oResponse) {
            foreach ($oResponse->getFiles() as $aFile) {
                $sFileRealName = Yii::app()->getConfig('uploaddir') . '/surveys/' . $iSurveyID . '/files/' . $aFile['filename'];

                if (!file_exists($sFileRealName)) {
                    return array('status' => 'Could not find uploaded files');
                }

                $uploaded_files[$aFile['filename']] = array(
                    'meta' => $aFile,
                    'content' => base64_encode(file_get_contents($sFileRealName))
                );
            }
        }

        return $uploaded_files;
    }

    /**
     * Get survey fieldmap (RPC function)
     *
     * Returns the requested survey's fieldmap in an array
     *
     * @access public
     * @param string $sessionKey Auth credentials
     * @param int $surveyId ID of the Survey
     * @param string $language (optional) language of the survey to use (default from Survey)
     * @return array
     */
    public function get_fieldmap($sessionKey, $surveyId, $language = null)
    {
        if (!$this->_checkSessionKey($sessionKey)) {
            return ['status' => self::INVALID_SESSION_KEY];
        }
        $surveyId = (int) $surveyId;
        $survey = Survey::model()->findByPk($surveyId);
        if (!isset($survey)) {
            return ['status' => 'Error: Invalid survey ID'];
        }
        if (!Permission::model()->hasSurveyPermission($surveyId, 'surveycontent', 'read')) {
            return ['status' => 'No permission'];
        }
        if (empty($language) || !in_array($language, $survey->allLanguages)) {
            $language = $survey->language;
        }
        // Get the fieldmap
        $fieldmap = createFieldMap($survey, 'full', false, false, $language);
        if (empty($fieldmap)) {
            return ['status' => 'Can not obtain field map'];
        }
        return $fieldmap;
    }

    /**
     * Login with username and password
     *
     * @access protected
     * @param string $sUsername username
     * @param string $sPassword password
     * @param string $sPlugin plugin to be used
     * @return bool|string
     */
    protected function _doLogin($sUsername, $sPassword, $sPlugin)
    {
        /* @var $identity LSUserIdentity */
        $identity = new LSUserIdentity($sUsername, $sPassword);
        $identity->setPlugin($sPlugin);
        $event = new PluginEvent('remoteControlLogin');
        $event->set('identity', $identity);
        $event->set('plugin', $sPlugin);
        $event->set('username', $sUsername);
        $event->set('password', $sPassword);
        App()->getPluginManager()->dispatchEvent($event, array($sPlugin));
        if (!$identity->authenticate()) {
            if ($identity->errorMessage) {
                // don't return an empty string
                return $identity->errorMessage;
            }
            return false;
        } else {
            return true;
        }
    }

    /**
     * Fills the session with necessary user info on the fly
     *
     * @access protected
     * @param string $username The username
     * @return bool
     */
    protected function _jumpStartSession($username)
    {
        $aUserData = User::model()->findByAttributes(array('users_name' => (string) $username))->attributes;

        $session = array(
            'loginID' => intval($aUserData['uid']),
            'user' => $aUserData['users_name'],
            'full_name' => $aUserData['full_name'],
            'htmleditormode' => $aUserData['htmleditormode'],
            'templateeditormode' => $aUserData['templateeditormode'],
            'questionselectormode' => $aUserData['questionselectormode'],
            'dateformat' => $aUserData['dateformat'],
            'adminlang' => 'en'
        );
        foreach ($session as $k => $v) {
            Yii::app()->session[$k] = $v;
        }
        Yii::app()->user->setId($aUserData['uid']);

        return true;
    }

    /**
     * Check if the session key is valid. If yes returns true, otherwise false and sends an error message with error code 1
     *
     * @access protected
     * @param string $sSessionKey Auth credentials
     * @return bool
     */
    protected function _checkSessionKey($sSessionKey)
    {
        $sSessionKey = (string) $sSessionKey;
        $criteria = new CDbCriteria();
        $criteria->condition = 'expire < ' . time();
        Session::model()->deleteAll($criteria);
        $oResult = Session::model()->findByPk($sSessionKey);

        if (is_null($oResult)) {
            return false;
        } else {
            $this->_jumpStartSession($oResult->data);
            return true;
        }
    }

    /**
     * Import a participant into the LimeSurvey CPDB
     *
     * It stores attributes as well, if they are registered before within ui
     *
     * Call the function with $response = $myJSONRPCClient->cpd_importParticipants( $sessionKey, $aParticipants);
     *
     * @param int $sSessionKey
     * @param array $aParticipants
     * [[0] => ["email"=>"dummy-02222@limesurvey.com","firstname"=>"max","lastname"=>"mustermann"]]
     * @param bool $update
     * @return array with status
     */
    public function cpd_importParticipants($sSessionKey, $participants, $update = false)
    {
        if (!$this->_checkSessionKey($sSessionKey)) {
            return array('status' => self::INVALID_SESSION_KEY);
        }

        $aDefaultFields = array('participant_id', 'firstname', 'lastname', 'email', 'language', 'blacklisted');
        $aResponse = array(
            'ImportCount' => 0,
            'UpdateCount' => 0
        );

        $aAttributeRecords = ParticipantAttributeName::model()
                                 ->with('participant_attribute_names_lang')
                                 ->findAll();

        foreach ($participants as $participant) {
            $model = null;

            if (isset($participant['id'])) {
                $participant['participant_id'] = $participant['id'];
            }

            if (isset($participant['participant_id'])) {
                $model = Participant::model()->findByPk($participant['participant_id']);
            } else {
                $model = Participant::model()->findByAttributes(array(
                    'firstname' => $participant['firstname'],
                    'lastname' => $participant['lastname'],
                    'email' => $participant['email'],
                    'owner_uid' => Yii::app()->session['loginID']
                ));
            }

            // Participant not found, so we create a new one
            if (!$model) {
                if (!Permission::model()->hasGlobalPermission('participantpanel', 'create')) {
                    /* No permission to create : continue */
                    continue;
                }
                $model = new Participant();
                if (isset($participant['participant_id'])) {
                    $model->participant_id = $participant['participant_id'];
                } else {
                    $model->participant_id = Participant::genUuid();
                }
            } elseif (!$model->userHasPermissionToEdit()) {
                /* No permission to update : continue */
                continue;
            }

            $scenario = $model->getScenario();  // insert or update
            if ($scenario == 'update' && $update === false) {
                continue;
            }

            $model->firstname = $participant['firstname'];
            $model->lastname = $participant['lastname'];
            $model->email = $participant['email'];
            $model->language = $participant['language'] ?? 'en';
            $model->owner_uid = Yii::app()->session['loginID'];
            $model->blacklisted = (isset($participant['blacklisted']) && $participant['blacklisted'] === 'Y') ? 'Y' : 'N';

            if ($scenario == 'insert') {
                $model->created = date('Y-m-d H:i:s');
                $model->created_by = Yii::app()->session['loginID'];
            } else {  // update
                $model->modified = date('Y-m-d H:i:s');
            }

            if ($model->encryptSave()) {
                foreach ($participant as $sLabel => $sAttributeValue) {
                    if (!in_array($sLabel, $aDefaultFields)) {
                        foreach ($aAttributeRecords as $sKey => $arValue) {
                            $aAttributes = $arValue->getAttributes();
                            if ($aAttributes['defaultname'] == $sLabel) {
                                $attribute = ParticipantAttribute::model();
                                $attribute->attribute_id = $aAttributes['attribute_id'];
                                $attribute->participant_id = $model->participant_id;
                                $attribute->value = $sAttributeValue;
                                $attribute->encrypt();
                                $attribute->updateParticipantAttributeValue($attribute->attributes);
                            }
                        }
                    }
                }

                if ($scenario == 'insert') {
                    $aResponse['ImportCount']++;
                } else {
                    $aResponse['UpdateCount']++;
                }
            }
        }

        return $aResponse;
    }

    /**
     * Check the email, if it's in a valid format
     * @param $sEmail
     * @return bool
     */
    protected function _checkEmailFormat($sEmail)
    {
        if ($sEmail != '') {
            $aEmailAddresses = explode(';', (string) $sEmail);
            // Ignore additional email addresses
            $sEmailaddress = $aEmailAddresses[0];
            if (!validateEmailAddress($sEmailaddress)) {
                return false;
            }
            return true;
        }
        return false;
    }

    /**
     * Safely apply conditions to a CDbCriteria object.
     * Hardens against SQL injection by validating column names.
     * @param model $oModel : can be \Token or \Survey or anything else
     * @param \LSDbCriteria $oCriteria
     * @param array $aConditions conditions to limit the list, either as a
     *              key => value if key is an integer : value is used as comparaison string : sample ['tid = 2']
     *              key=>value search value in column key  : sample ['tid' => '2']
     *              key=>array(operator,value[,value[...]]) using an operator : sample ['tid'=>['=','2']]
     *                  Valid operators are  ['<', '>', '>=', '<=', '=', '<>', 'LIKE', 'IN']
     *                  Only the IN operator allows for several values.
     *              All conditions are connected by AND.
     * @return null|string if string it's an error.
     */
    protected function addConditionsToCriteria($oModel, $oCriteria, $aConditions = [])
    {
        if (empty($aConditions)) {
            return null;
        }
        foreach ($aConditions as $columnName => $valueOrTuple) {
            /* Indexed element */
            if (is_int($columnName)) {
                try {
                    $oCriteria->addSafeStringSearchCondition($oModel, $valueOrTuple);
                } catch (Exception $e) {
                    return $e->getMessage();
                }
            /* Associative element */
            } else {
                try {
                    $oCriteria->addSafeStructuredSearchCondition($oModel, [$columnName => $valueOrTuple]);
                } catch (Exception $e) {
                    return $e->getMessage();
                }
            }
        }
        return null;
    }
}
