HEX
Server: Apache
System: Linux WWW 6.1.0-40-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.1.153-1 (2025-09-20) x86_64
User: web11 (1011)
PHP: 8.2.29
Disabled: NONE
Upload Files
File: /var/www/apklausos/application/controllers/QuestionAdministrationController.php
<?php

use LimeSurvey\Models\Services\QuestionAggregateService;
use LimeSurvey\Models\Services\Exception\{
    NotFoundException,
    PermissionDeniedException,
    QuestionHasConditionsException
};

/**
 * Class QuestionAdministrationController
 */
class QuestionAdministrationController extends LSBaseController
{
    /**
     * It's import to have the accessRules set (security issue).
     * Only logged in users should have access to actions. All other permissions
     * should be checked in the action itself.
     *
     * @return array
     */
    public function accessRules()
    {
        return [
            [
                'allow',
                'actions' => [],
                'users'   => ['*'], //everybody
            ],
            [
                'allow',
                'actions' => ['view'],
                'users'   => ['@'], //only login users
            ],
            ['deny'], //always deny all actions not mentioned above
        ];
    }

    /**
     * This part comes from renderWrappedTemplate
     *
     * @param string $view View
     *
     * @return bool
     */
    protected function beforeRender($view)
    {
        if (isset($this->aData['surveyid'])) {
            $this->aData['oSurvey'] = $this->aData['oSurvey'] ?? Survey::model()->findByPk($this->aData['surveyid']);

            // Needed to evaluate EM expressions in question summary
            // See bug #11845
            LimeExpressionManager::SetSurveyId($this->aData['surveyid']);
            LimeExpressionManager::StartProcessingPage(false, true);

            $this->layout = 'layout_questioneditor';
        }

        return parent::beforeRender($view);
    }

    /**
     * Renders the main view for question editor.
     * Main view function prepares the necessary global js parts and renders the HTML for the question editor
     *
     * @param integer $surveyid          Survey ID
     * @param integer $gid               Group ID
     * @param integer $qid               Question ID
     * @param string  $landOnSideMenuTab Name of the side menu tab. Default behavior is to land on structure tab.
     *
     * @return void
     *
     * @throws CException
     */
    public function actionView($surveyid, $gid = null, $qid = null, $landOnSideMenuTab = 'structure')
    {
        SettingsUser::setUserSetting('last_question', $qid);
        $this->actionEdit($qid);
    }

    /**
     * Show form to create new question.
     *
     * @param int $surveyid
     * @return void
     */
    public function actionCreate($surveyid)
    {
        $surveyid = (int) $surveyid;

        if (!Permission::model()->hasSurveyPermission($surveyid, 'surveycontent', 'create')) {
            App()->user->setFlash('error', gT("Access denied"));
            $this->redirect(App()->request->urlReferrer);
        }

        $oSurvey = Survey::model()->findByPk($surveyid);
        if (empty($oSurvey)) {
            throw new Exception('Internal error: Found no survey with id ' . $surveyid);
        }

        $oQuestion = $this->getQuestionObject();
        $oQuestion->sid = $surveyid;

        $this->aData['showSaveAndNewGroupButton'] = true;
        $this->aData['showSaveAndNewQuestionButton'] = true;
        $this->aData['closeUrl'] = Yii::app()->createUrl(
            'questionAdministration/listquestions',
            [
                'surveyid' => $surveyid
            ]
        );

        $this->aData['tabOverviewEditor'] = 'overview';
        $this->renderFormAux($oQuestion);
    }

    /**
     * Show question edit form.
     *
     * @param int    $questionId        Question ID
     * @param string $tabOverviewEditor which tab should be used this can be 'overview' or 'editor'
     * @return void
     * @throws CHttpException
     */
    public function actionEdit(int $questionId, string $tabOverviewEditor = null)
    {
        $questionId = (int) $questionId;
        if (!in_array($tabOverviewEditor, ['overview', 'editor'], true)) {
            $tabOverviewEditor = null;
        }

        /** @var $question Question|null */
        $question = Question::model()->findByPk($questionId);
        if (empty($question)) {
            throw new CHttpException(404, gT("Invalid question id"));
        }

        if (!Permission::model()->hasSurveyPermission($question->sid, 'surveycontent', 'update')) {
            Yii::app()->user->setFlash('error', gT("Access denied"));
            $this->redirect(Yii::app()->request->urlReferrer);
        }

        // "Directly show edit mode" personal setting
        if (is_null($tabOverviewEditor)) {
            $tabOverviewEditor = SettingsUser::getUserSettingValue('noViewMode', App()->user->id) ? 'editor' : 'overview';
        }

        $this->aData['closeUrl'] = Yii::app()->createUrl(
            'questionAdministration/view/',
            [
                'surveyid' => $question->sid,
                'gid' => $question->gid,
                'qid' => $question->qid,
                'landOnSideMenuTab' => 'structure'
            ]
        );
        $this->aData['tabOverviewEditor'] = $tabOverviewEditor;
        $this->renderFormAux($question);
    }

    /**
     * Helper function to render form.
     * Used by create and edit actions.
     *
     * @param Question $question Question
     * @return void
     * @throws CException
     * @todo Move to service class
     */
    private function renderFormAux(Question $question)
    {
        Yii::app()->loadHelper("admin.htmleditor");
        Yii::app()->getClientScript()->registerPackage('ace');
        Yii::app()->getClientScript()->registerPackage('jquery-ace');
        Yii::app()->getClientScript()->registerScript(
            'editorfiletype',
            "editorfiletype ='javascript';",
            CClientScript::POS_HEAD
        );
        App()->getClientScript()->registerScriptFile(
            App()->getConfig('adminscripts') . 'questionEditor.js',
            CClientScript::POS_END
        );
        // TODO: No difference between true and false?
        PrepareEditorScript(false, $this);
        App()->session['FileManagerContext'] = "edit:survey:{$question->sid}";
        initKcfinder();

        $this->aData['surveyid'] = $question->sid;
        $this->aData['sid'] = $question->sid;
        $this->aData['display']['menu_bars']['gid_action'] = 'viewquestion';
        $this->aData['questionbar']['buttons']['view'] = true;
        $this->aData['sidemenu']['landOnSideMenuTab'] = 'structure';
        $this->aData['title_bar']['title'] =
            $question->survey->currentLanguageSettings->surveyls_title
            . " (" . gT("ID") . ":" . $question->sid . ")";
        $this->aData['aQuestionTypeList'] = QuestionTheme::findAllQuestionMetaDataForSelector();
        $advancedSettings = $this->getAdvancedOptions($question->qid, $question->type, $question->question_theme_name);
        // Remove general settings from this array.
        unset($advancedSettings['Attribute']);

        // Add <input> with JSON as value, used by JavaScript.
        $jsVariablesHtml = $this->renderPartial(
            '/admin/survey/Question/_subQuestionsAndAnwsersJsVariables',
            [
                'qid'               => $question->qid,
                'anslangs'          => $question->survey->allLanguages,
                // TODO
                'assessmentvisible' => false,
                'scalecount'        => $question->questionType->answerscales
            ],
            true
        );

        $showScriptField = Permission::model()->hasSurveyPermission($question->sid, 'surveycontent', 'update') &&
            SettingsUser::getUserSettingValue('showScriptEdit', App()->user->id, null, null, 1);

        // TODO: Problem with CSRF cookie when entering directly after login.
        $modalsHtml =  Yii::app()->twigRenderer->renderViewFromFile(
            '/application/views/questionAdministration/modals.twig',
            [],
            true
        );

        // Top Bar
        $this->aData['topBar']['name'] = 'questionTopbar_view';

        // Save Button
        $this->aData['showSaveButton'] = true;

        // Save and Close Button
        $this->aData['showSaveAndCloseButton'] = true;

        // Close Button
        $this->aData['showCloseButton'] = true;

        // Delete Button
        $this->aData['showDeleteButton'] = true;

        $this->aData['sid'] = $question->sid;
        $this->aData['gid'] = $question->gid;
        $this->aData['qid'] = $question->qid;

        $this->aData['hasdefaultvalues'] = (QuestionTheme::findQuestionMetaData($question->type)['settings'])->hasdefaultvalues;

        $generalSettings = $this->getGeneralOptions(
            $question->qid,
            $question->type,
            $question->gid,
            $question->question_theme_name
        );

        $selectormodeclass = $this->getSelectorModeClass();

        $viewData = [
            'oSurvey'                => $question->survey,
            'oQuestion'              => $question,
            'aQuestionTypeGroups'    => $this->getQuestionTypeGroups($this->aData['aQuestionTypeList']),
            'advancedSettings'       => $advancedSettings,
            'generalSettings'        => $generalSettings,
            'showScriptField'       => $showScriptField,
            'jsVariablesHtml'       => $jsVariablesHtml,
            'modalsHtml'            => $modalsHtml,
            'selectormodeclass'     => $selectormodeclass,
        ];

        $this->aData = array_merge($this->aData, $viewData);

        $this->render(
            'create',
            $viewData
        );
    }

    public function actionAjaxLoadExtraOptions($questionId)
    {
        $questionId = (int) $questionId;
        $question = Question::model()->findByPk($questionId);
        if (empty($question)) {
            throw new CHttpException(404, gT('Invalid question id'));
        }

        if (!Permission::model()->hasSurveyPermission($question->sid, 'surveycontent', 'read')) {
            Yii::app()->user->setFlash('error', gT("Access denied"));
            $this->redirect(Yii::app()->request->urlReferrer);
        }
        Yii::app()->loadHelper("admin.htmleditor");
        PrepareEditorScript(false, $this);
        App()->session['FileManagerContext'] = "edit:survey:{$question->sid}";
        initKcfinder();

        $this->renderPartial(
            'extraOptions',
            [
                'question' => $question,
                'survey' => $question->survey,
            ]
        );
    }

    /**
     * Load list questions view for a specified survey by $surveyid
     *
     * @param int $surveyid Goven Survey ID
     * @param string  $landOnSideMenuTab Name of the side menu tab (settings or structure). Default behavior is to land on settings tab.
     *
     * @return string
     * @access public
     * @todo   php warning (Missing return statement)
     */
    public function actionListQuestions($surveyid, $landOnSideMenuTab = 'settings')
    {
        if (!Permission::model()->hasSurveyPermission($surveyid, 'surveycontent', 'read')) {
            throw new CHttpException(403, gT("No permission"));
        }
        $iSurveyID = sanitize_int($surveyid);
        if (!in_array($landOnSideMenuTab, ['settings', 'structure', ''])) {
            $landOnSideMenuTab = 'settings';
        }
        // Reinit LEMlang and LEMsid: ensure LEMlang are set to default lang, surveyid are set to this survey ID
        // Ensure Last GetLastPrettyPrintExpression get info from this sid and default lang
        LimeExpressionManager::SetEMLanguage(Survey::model()->findByPk($iSurveyID)->language);
        LimeExpressionManager::SetSurveyId($iSurveyID);
        LimeExpressionManager::StartProcessingPage(false, true);

        // Set number of page
        $pageSize = App()->request->getParam('pageSize', null);
        if ($pageSize != null) {
            App()->user->setState('pageSize', (int) $pageSize);
        }

        $oSurvey = Survey::model()->findByPk($iSurveyID);
        $aData   = array();

        $aData['oSurvey']                               = $oSurvey;
        $aData['surveyid']                              = $iSurveyID;
        $aData['sid']                                   = $iSurveyID;
        $aData['sidemenu']['listquestions']             = true;
        $aData['sidemenu']['landOnSideMenuTab']         = $landOnSideMenuTab;
        $aData['surveybar']['returnbutton']['url']      = "/surveyAdministration/listsurveys";

        $aData["surveyHasGroup"]        = $oSurvey->groups;
        $aData['subaction']             = gT("Questions in this survey");
        $aData['title_bar']['title']    = $oSurvey->currentLanguageSettings->surveyls_title .
            " (" . gT("ID") . ":" . $iSurveyID . ")";

        // The DataProvider will be build from the Question model, search method
        $questionModel = new Question('search');
        // Global filter
        if (isset($_GET['Question'])) {
            $questionModel->setAttributes($_GET['Question'], false);
        }
        // Filter group
        if (isset($_GET['gid'])) {
            $questionModel->gid = $_GET['gid'];
        }
        // Set number of page
        if (isset($_GET['pageSize'])) {
            App()->user->setState('pageSize', (int) $_GET['pageSize']);
        }
        $aData['pageSize'] = App()->user->getState('pageSize', App()->params['defaultPageSize']);
        // We filter the current survey ID
        $questionModel->sid = $oSurvey->sid;
        $aData['questionModel'] = $questionModel;

        $aData['surveyid'] = $iSurveyID;
        $aData['surveybar'] = [];

        // for newly combined groups and reorder parts
        $diContainer = \LimeSurvey\DI::getContainer();
        $questionGroupService = $diContainer->get(
            LimeSurvey\Models\Services\QuestionGroupService::class
        );

        if (App()->request->getParam('pageSize', 0) > 0) {
            App()->user->setState('pageSize', (int)$pageSize);
        }
        $aData['groupModel'] = $questionGroupService->getGroupData(
            $aData['oSurvey'],
            App()->request->getParam('QuestionGroup', [])
        );
        $aData['aGroupsAndQuestions'] = $this->getReorderData($oSurvey);
        $aData['surveyActivated'] = $oSurvey->getIsActive();
        $this->aData = $aData;

         $aData['hasSurveyContentCreatePermission'] = Permission::model()->hasSurveyPermission(
             $iSurveyID,
             'surveycontent',
             'create'
         );


        $this->render("listquestions", $aData);
    }

    public function getReorderData($oSurvey)
    {
        $iSurveyID = $oSurvey->primaryKey;
        $baselang = $oSurvey->language;
        // cloned below content from surveyAdministrationController line#2550
        $groups = $oSurvey->groups;
        $groupData = [];
        $initializedReplacementFields = false;
        foreach ($groups as $iGID => $oGroup) {
            $groupData[$iGID]['gid'] = $oGroup->gid;
            $groupData[$iGID]['group_text'] = $oGroup->gid . ' ' . $oGroup->questiongroupl10ns[$baselang]->group_name;
            LimeExpressionManager::StartProcessingGroup($oGroup->gid, false, $iSurveyID);
            if (!$initializedReplacementFields) {
                templatereplace("{SITENAME}"); // Hack to ensure the EM sets values of LimeReplacementFields
                $initializedReplacementFields = true;
            }

            $qs = array();

            foreach ($oGroup->questions as $question) {
                $relevance = $question->relevance == '' ? 1 : $question->relevance;
                $questionText = sprintf(
                    '[{%s}] %s % s',
                    $relevance,
                    $question->title,
                    $question->questionl10ns[$baselang]->question
                );
                LimeExpressionManager::ProcessString($questionText, $question->qid);
                $questionData['question'] = viewHelper::stripTagsEM(LimeExpressionManager::GetLastPrettyPrintExpression());
                $questionData['gid'] = $oGroup->gid;
                $questionData['qid'] = $question->qid;
                $questionData['title'] = $question->title;
                $qs[] = $questionData;
            }
            $groupData[$iGID]['questions'] = $qs;
            LimeExpressionManager::FinishProcessingGroup();
        }

        return $groupData;
    }

    /****
     * *** A lot of getter function regarding functionalities and views.
     * *** All called via ajax
     ****/

    /**
     * Returns all languages in a specific survey as a JSON document
     *
     * @todo is this action still in use?? where in the frontend?
     *
     * @param int $iSurveyId
     *
     * @return void
     */
    public function actionGetPossibleLanguages($iSurveyId)
    {
        $iSurveyId = (int)$iSurveyId;
        $aLanguages = Survey::model()->findByPk($iSurveyId)->allLanguages;
        $this->renderJSON($aLanguages);
    }

    /**
     * Action called by the FE editor when a save is triggered.
     * This is called for both new question and update.
     *
     * @return void
     * @throws CException
     */
    public function actionSaveQuestionData()
    {
        $request = App()->request;
        $calledWithAjax = (int) $request->getPost('ajax');
        $sScenario = $request->getPost('scenario', '');
        $surveyId = (int) $request->getPost('sid');

        // Check the POST data is not truncated
        if (!$request->getPost('bFullPOST')) {
            $message = gT('The data received seems incomplete. This usually happens due to server limitations (PHP setting max_input_vars). Please contact your system administrator.');
            if ($calledWithAjax) {
                echo json_encode(['message' => $message]);
                Yii::app()->end();
            } else {
                $sRedirectUrl = $this->createUrl(
                    'questionAdministration/listQuestions',
                    ['surveyid' => $surveyId]
                );
                Yii::app()->setFlashMessage($message, 'error');
                $this->redirect($sRedirectUrl);
            }
        }

        $data = !empty($_POST) ? $_POST : [];

        $diContainer = \LimeSurvey\DI::getContainer();
        $questionAggregateService = $diContainer->get(
            QuestionAggregateService::class
        );

        $question = null;
        try {
            $question = $questionAggregateService->save(
                $surveyId,
                $data
            );

            $tabOverviewEditorValue = $request->getPost('tabOverviewEditor');
            // only those two values are valid
            if (
                !(
                    $tabOverviewEditorValue === 'overview'
                    || $tabOverviewEditorValue === 'editor'
                )
            ) {
                $tabOverviewEditorValue = 'overview';
            }

            if ($calledWithAjax) {
                echo json_encode(
                    ['message' => gT('Question saved')]
                );
                Yii::app()->end();
            } else {
                App()->setFlashMessage(
                    gT('Question saved'),
                    'success'
                );
                $landOnSideMenuTab = 'structure';
                if (empty($sScenario)) {
                    if (
                        App()->request
                            ->getPost('save-and-close', '')
                    ) {
                        $sScenario = 'save-and-close';
                    } elseif (
                        App()->request
                            ->getPost('saveandnew', '')
                    ) {
                        $sScenario = 'save-and-new';
                    } elseif (
                        App()->request
                            ->getPost('saveandnewquestion', '')
                    ) {
                        $sScenario = 'save-and-new-question';
                    }
                }
                switch ($sScenario) {
                    case 'save-and-new-question':
                        $sRedirectUrl = $this->createUrl(
                            // TODO: Double check
                            'questionAdministration/create/',
                            [
                                'surveyid' => $surveyId,
                                'gid' => $question->gid,
                            ]
                        );
                        break;
                    case 'save-and-new':
                        $sRedirectUrl = $this->createUrl(
                            'questionGroupsAdministration/add/',
                            [
                                'surveyid' => $surveyId,
                            ]
                        );
                        break;
                    case 'save-and-close':
                        $sRedirectUrl = $this->createUrl(
                            'questionGroupsAdministration/view/',
                            [
                                'surveyid' => $surveyId,
                                'gid' => $question->gid,
                                'landOnSideMenuTab' => $landOnSideMenuTab,
                                'mode' => 'overview'
                            ]
                        );
                        break;
                    default:
                        $sRedirectUrl = $this->createUrl(
                            'questionAdministration/edit/',
                            [
                                'questionId' => $question->qid,
                                'landOnSideMenuTab' => $landOnSideMenuTab,
                                'tabOverviewEditor' => $tabOverviewEditorValue
                            ]
                        );
                }
                $this->redirect($sRedirectUrl);
            }
        } catch (PermissionDeniedException $e) {
            Yii::app()->user->setFlash('error', gT('Access denied'));
            $this->redirect(Yii::app()->request->urlReferrer);
        } catch (\Exception $e) {
            // Determine the proper redirect URL
            if (empty($question)) {
                $redirectUrl = $this->createUrl(
                    'surveyAdministration/view/',
                    ["surveyid" => $surveyId]
                );
            } else {
                $tabOverviewEditorValue = $request->getPost('tabOverviewEditor');
                if (
                    $tabOverviewEditorValue !== 'overview'
                    && $tabOverviewEditorValue !== 'editor'
                ) {
                    $tabOverviewEditorValue = 'overview';
                }
                $redirectUrl = $this->createUrl(
                    'questionAdministration/edit/',
                    [
                        'questionId' => $question->qid,
                        'landOnSideMenuTab' => 'structure',
                        'tabOverviewEditor' => $tabOverviewEditorValue,
                    ]
                );
            }

            // If we are already dealing with a friendly exception
            // (may include detailed errors),
            // just set the redirect URL and re-throw.
            if ($e instanceof LSUserException) {
                throw $e->setRedirectUrl($redirectUrl);
            }

            throw new LSUserException(
                500,
                $e->getMessage(),
                0,
                $redirectUrl
            );
        }
    }

    /**
     * @todo document me
     *
     * @param int $iQuestionId
     * @param string $sQuestionType
     * @param int $gid
     * @param boolean $returnArray
     * @param string $questionThemeName
     *
     * @return void|array
     * @throws CException
     */
    public function actionGetGeneralOptions(
        $iQuestionId = null,
        $sQuestionType = null,
        $gid = null,
        $returnArray = false,  //todo see were this ajaxrequest is done and take out the parameter there and here
        $questionThemeName = null
    ) {
        $aGeneralOptionsArray = $this->getGeneralOptions($iQuestionId, $sQuestionType, $gid, $questionThemeName);

        $this->renderJSON($aGeneralOptionsArray);
    }


    /**
     * Collect initial question data
     * This either creates a temporary question object, or calls a question object from the database
     *
     * @param int $iQuestionId
     * @param int $gid
     * @param string $type
     *
     * @return void
     * @throws CException
     */
    public function actionGetQuestionData($iQuestionId = null, $gid = null, $type = null)
    {
        $iQuestionId = (int)$iQuestionId;
        $oQuestion = $this->getQuestionObject($iQuestionId, $type, $gid);

        $aQuestionInformationObject = $this->getCompiledQuestionData($oQuestion);
        $surveyInfo = $this->getCompiledSurveyInfo($oQuestion);

        $aLanguages = [];
        $aAllLanguages = getLanguageData(false, App()->session['adminlang']);
        $aSurveyLanguages = $oQuestion->survey->getAllLanguages();
        array_walk(
            $aSurveyLanguages,
            function ($lngString) use (&$aLanguages, $aAllLanguages) {
                $aLanguages[$lngString] = $aAllLanguages[$lngString]['description'];
            }
        );

        $this->renderJSON(
            array_merge(
                $aQuestionInformationObject,
                [
                    'surveyInfo'   => $surveyInfo,
                    'languages'    => $aLanguages,
                    'mainLanguage' => $oQuestion->survey->language,
                ]
            )
        );
    }

    /**
     * Called via Ajax.
     *
     * @param int $surveyid
     * @param int $gid
     * @param string $codes
     * @param int $scale_id
     * @param int $position
     * @param string $assessmentvisible
     * @return void
     * @todo Permission check hard when both sid and gid are given.
     */
    public function actionGetSubquestionRowForAllLanguages($surveyid, $gid, $codes, $scale_id, $position = 0, $assessmentvisible = '')
    {
        $oSurvey = Survey::model()->findByPk($surveyid);
        if (empty($oSurvey)) {
            throw new CHttpException(404, gT("Invalid survey ID"));
        }
        if (!Permission::model()->hasSurveyPermission($oSurvey->sid, 'surveycontent', 'update')) {
            throw new CHttpException(403, gT("No permission"));
        }
        $html  = [];
        $first = true;
        $qid   = App()->getRequest()->getParam('subqid') ?? 'new' . rand(0, 99999);
        foreach ($oSurvey->allLanguages as $language) {
            $html[$language] = $this->getSubquestionRow(
                $oSurvey->sid,
                $gid,
                $qid,
                $codes,
                $language,
                $first,
                $scale_id,
                $position,
                $assessmentvisible
            );
            $first = false;
        }
        header('Content-Type: application/json');
        echo json_encode($html);
    }

    /**
     * AJAX Method to QuickAdd multiple Rows AJAX-based
     * @todo Permission
     * @todo Should be GET, not POST
     * @return void
     */
    public function actionGetSubquestionRowQuickAdd($surveyid, $gid)
    {
        $qid               = '-QUIDPLACEHOLDER-';
        $request           = Yii::app()->request;
        $codes             = $request->getPost('codes');
        $language          = $request->getPost('language');
        $first             = $request->getPost('first');
        $scale_id          = $request->getPost('scale_id');
        $type              = $request->getPost('type');
        $position          = $request->getPost('position');
        $assessmentvisible = $request->getPost('assessmentvisible');
        echo $this->getSubquestionRow($surveyid, $gid, $qid, $codes, $language, $first, $scale_id, $position, $assessmentvisible);
    }

    /**
     * @todo Permission
     * @todo Should be GET, not POST
     * @return void
     */
    public function actionGetAnswerOptionRowQuickAdd($surveyid, $gid)
    {
        $qid               = '-QUIDPLACEHOLDER-';
        $request           = Yii::app()->request;
        $codes             = $request->getPost('codes');
        $language          = $request->getPost('language');
        $first             = $request->getPost('first');
        $scale_id          = $request->getPost('scale_id');
        $type              = $request->getPost('type');
        $position          = $request->getPost('position');
        $assessmentvisible = $request->getPost('assessmentvisible');
        echo $this->getAnswerOptionRow($surveyid, $gid, $qid, $codes, $language, $first, $scale_id, $position, $assessmentvisible);
    }

    /**
     * @return void
     */
    public function actionGetAnswerOptionRowForAllLanguages($surveyid, $gid, $codes, $scale_id, $position = 0, $assessmentvisible = '')
    {
        $oSurvey = Survey::model()->findByPk($surveyid);
        if (empty($oSurvey)) {
            throw new CHttpException(404, gT("Invalid survey ID"));
        }
        if (!Permission::model()->hasSurveyPermission($oSurvey->sid, 'surveycontent', 'update')) {
            throw new CHttpException(403, gT("No permission"));
        }
        $html  = [];
        $first = true;
        $qid   = App()->getRequest()->getParam('subqid') ?? 'new' . rand(0, 99999);
        foreach ($oSurvey->allLanguages as $language) {
            $html[$language] = $this->getAnswerOptionRow(
                $oSurvey->sid,
                $gid,
                $qid,
                $codes,
                $language,
                $first,
                $scale_id,
                $position,
                $assessmentvisible
            );
            $first = false;
        }
        header('Content-Type: application/json');
        echo json_encode($html);
    }

    /**
     * It returns a empty subquestion row.
     * Used when user clicks "Add new row" in question editor.
     *
     * @todo Document.
     * @todo Too many arguments.
     * @param string $codes All previous codes (used to calculate the next code)
     * @return string
     */
    private function getSubquestionRow($surveyid, $gid, $qid, $codes, $language, $first, $scale_id, $position, $assessmentvisible = '')
    {
        // index.php/admin/questions/sa/getSubquestionRow/position/1/scale_id/1/surveyid/691948/gid/76/qid/1611/language/en/first/true

        // TODO: calcul correct value
        $oldCode = false;

        // TODO: Fix question type 'A'. Needed?
        $oQuestion = $this->getQuestionObject($qid, 'A', $gid);
        $oSubquestion = $oQuestion->getEmptySubquestion();
        $oSubquestion->qid = $qid;  // Set qid as new12345 random id.

        //Capture "true" and "false" as strings
        if (is_string($first)) {
            $first = $first == "false" ? false : true;
        }

        $stringCodes = json_decode($codes, true);
        list($oSubquestion->title, $newPosition) = $this->calculateNextCode($stringCodes);

        $activated = false; // You can't add ne subquestion when survey is active
        Yii::app()->loadHelper('admin/htmleditor'); // Prepare the editor helper for the view

        $view = 'subquestionRow.twig';
        $aData = array(
            'position'  => $position,
            'scale_id'  => $scale_id,
            'activated' => $activated,
            'first'     => $first,
            'surveyid'  => $surveyid,
            'gid'       => $gid,
            'qid'       => $qid,
            'language'  => $language,
            'question'  => '',
            'relevance' => '1',
            'oldCode'   => $oldCode,
            'subquestion'  => $oSubquestion
        );

        $html = '<!-- Inserted Row -->';
        $html .= App()->twigRenderer->renderPartial('/questionAdministration/' . $view, $aData);
        $html .= '<!-- end of Inserted Row -->';
        return $html;
    }

    /**
     * @todo docs
     * @return string
     */
    private function getAnswerOptionRow($surveyid, $gid, $qid, $codes, $language, $first, $scale_id, $position, $assessmentvisible = '')
    {
        $oldCode = false;

        // @todo Fix question type 'A'. Needed?
        $oQuestion = $this->getQuestionObject($qid, 'A', $gid);
        $answerOption = $oQuestion->getEmptyAnswerOption();
        $answerOption->aid = $qid;

        //Capture "true" and "false" as strings
        if (is_string($first)) {
            $first = $first == "false" ? false : true;
        }

        $oSurvey = Survey::model()->findByPk($surveyid);
        $stringCodes = json_decode((string) $codes, true);
        list($answerOption->code, $newPosition) = $this->calculateNextCode($stringCodes);

        $activated = false; // You can't add ne subquestion when survey is active
        Yii::app()->loadHelper('admin/htmleditor'); // Prepare the editor helper for the view

        $view = 'answerOptionRow.twig';
        $aData = array(
            'assessmentvisible' => Assessment::isAssessmentActive($surveyid),
            'assessment_value'  => '',
            'answerOption'      => $answerOption,
            'answerOptionl10n'  => $answerOption->answerl10ns[$language],
            'sortorder'         => $newPosition,
            'position'          => $newPosition,
            'scale_id'          => $scale_id,
            'activated'         => $activated,
            'first'             => $first,
            'surveyid'          => $surveyid,
            'sid'               => $surveyid,
            'gid'               => $gid,
            'qid'               => $qid,
            'language'          => $language,
            'question'          => $oQuestion,
            'relevance'         => '1',
            'oldCode'           => $oldCode,
        );
        $html = '<!-- Inserted Row -->';
        $html .= App()->twigRenderer->renderPartial('/questionAdministration/' . $view, $aData);
        $html .= '<!-- end of Inserted Row -->';
        return $html;
    }

    /**
     * Calculate the next subquestion code based on previous codes.
     *
     * @param array $stringCodes
     * @return array
     */
    private function calculateNextCode(array $stringCodes)
    {
        if (empty($stringCodes)) {
            return ['A1', 0];
        }
        // We get the numerical part of each code and we store them in Arrays
        // One array is to store the pure numerical values (so we can search in it for the greates value, and increment it)
        // Another array is to store the string values (so we keep all the prefixed "0")
        $numCodes = array();
        foreach ($stringCodes as $key => $stringCode) {
            // This will loop into the code, from the last character to the first letter
            $numericSuffix = '';
            $n = 1;
            $numeric = true;
            while ($numeric === true && $n <= strlen((string) $stringCode)) {
                $currentCharacter = (string) substr((string) $stringCode, -$n, 1); // get the current character

                if (ctype_digit($currentCharacter)) {
                    // check if it's numerical
                    $numericSuffix = $currentCharacter . $numericSuffix; // store it in a string
                    $n = $n + 1;
                } else {
                    $numeric = false; // At first non numeric character found, the loop is stoped
                }
            }
            $numCodesWithZero[$key] = (string) $numericSuffix; // In string type, we can have   : "0001"
            $numCodes[$key]         = (int) $numericSuffix; // In int type, we can only have : "1"
        }

        // Let's get the greatest code
        $greatestNumCode          = max($numCodes); // greatest code
        $key                      = array_keys($numCodes, max($numCodes)); // its key (same key in all tables)
        $greatesNumCodeWithZeros  = (isset($numCodesWithZero)) ? $numCodesWithZero[$key[0]] : ''; // its value with prefixed 0 (like : 001)
        $stringCodeOfGreatestCode = $stringCodes[$key[0]]; // its original submitted  string (like: SQ001)

        // We get the string part of it: it's the original string code, without the greates code with its 0 :
        // like  substr ("SQ001", (strlen(SQ001)) - strlen(001) ) ==> "SQ"
        $stringPartOfNewCode    = (string) substr((string) $stringCodeOfGreatestCode, 0, (strlen((string) $stringCodeOfGreatestCode) - strlen($greatesNumCodeWithZeros)));

        // We increment by one the greatest code
        $numericalPartOfNewCode = $greatestNumCode + 1;

        // We get the list of 0 : (using $numericalPartOfNewCode will remove the excedent 0 ; SQ009 will be followed by SQ010 )
        $listOfZero = (string) substr($greatesNumCodeWithZeros, 0, (strlen($greatesNumCodeWithZeros) - strlen($numericalPartOfNewCode)));

        // When no more zero are available we want to be sure that the last 9 unit will not left
        // (like in SQ01 => SQ99 ; should become SQ100, not SQ9100)
        $listOfZero = $listOfZero == "9" ? '' : $listOfZero;

        // We finaly build the new code
        return [$stringPartOfNewCode . $listOfZero . $numericalPartOfNewCode, $numericalPartOfNewCode];
    }

    /**
     * Collect the permissions available for a specific question
     *
     * @param $iQuestionId
     *
     * @return void
     * @throws CException
     */
    public function actionGetQuestionPermissions($iQuestionId = null)
    {
        $iQuestionId = (int)$iQuestionId;
        $oQuestion = $this->getQuestionObject($iQuestionId);

        $aPermissions = [
            "read"         => Permission::model()->hasSurveyPermission($oQuestion->sid, 'surveycontent', 'read'),
            "update"       => Permission::model()->hasSurveyPermission($oQuestion->sid, 'surveycontent', 'update'),
            "editorpreset" => App()->session['htmleditormode'],
            "script"       =>
            Permission::model()->hasSurveyPermission($oQuestion->sid, 'surveycontent', 'update')
                && SettingsUser::getUserSetting('showScriptEdit', App()->user->id, null, null, 1),
        ];

        $this->renderJSON($aPermissions);
    }

    /**
     * Returns a json document containing the question types
     *
     * @return void
     */
    public function actionGetQuestionTypeList()
    {
        $this->renderJSON(QuestionType::modelsAttributes());
    }

    /**
     * @todo document me.
     * @todo is this used in frontend somewherer? can't find it
     *
     * @param string $sQuestionType
     * @return void
     */
    public function actionGetQuestionTypeInformation($sQuestionType)
    {
        $aTypeInformations = QuestionType::modelsAttributes();
        $aQuestionTypeInformation = $aTypeInformations[$sQuestionType];

        $this->renderJSON($aQuestionTypeInformation);
    }

    /**
     * Renders the top bar definition for questions as JSON document
     *
     * @param int $qid
     * @return false|null|string|string[]
     * @throws CException
     */
    public function actionGetQuestionTopbar($qid = null)
    {
        $oQuestion = $this->getQuestionObject($qid);
        $sid = $oQuestion->sid;
        $gid = $oQuestion->gid;
        $qid = $oQuestion->qid;
        $questionTypes = QuestionType::modelsAttributes();
        // TODO: Rename Variable for better readability.
        $qrrow = $oQuestion->attributes;
        $ownsSaveButton = true;
        $ownsImportButton = true;

        $hasCopyPermission = Permission::model()->hasSurveyPermission($sid, 'surveycontent', 'create');
        $hasUpdatePermission = Permission::model()->hasSurveyPermission($sid, 'surveycontent', 'update');
        $hasExportPermission = Permission::model()->hasSurveyPermission($sid, 'surveycontent', 'export');
        $hasDeletePermission = Permission::model()->hasSurveyPermission($sid, 'surveycontent', 'delete');
        $hasReadPermission = Permission::model()->hasSurveyPermission($sid, 'surveycontent', 'read');

        return $this->renderPartial(
            'question_topbar',
            [
                'oSurvey'             => $oQuestion->survey,
                'sid'                 => $sid,
                'hasCopyPermission'   => $hasCopyPermission,
                'hasUpdatePermission' => $hasUpdatePermission,
                'hasExportPermission' => $hasExportPermission,
                'hasDeletePermission' => $hasDeletePermission,
                'hasReadPermission'   => $hasReadPermission,
                'gid'                 => $gid,
                'qid'                 => $qid,
                'qrrow'               => $qrrow,
                'qtypes'              => $questionTypes,
                'ownsSaveButton'      => $ownsSaveButton,
                'ownsImportButton'    => $ownsImportButton,
            ],
            false,
            false
        );
    }

    /**
     * Display import view for Question
     *
     * @param int $surveyid
     * @param int|null $groupid
     */
    public function actionImportView($surveyid, $groupid = null)
    {
        $iSurveyID = (int)$surveyid;
        if (!Permission::model()->hasSurveyPermission($iSurveyID, 'surveycontent', 'import')) {
            App()->session['flashmessage'] = gT("We are sorry but you don't have permissions to do this.");
            $this->redirect(['questionAdministration/listquestions/surveyid/' . $iSurveyID]);
        }
        $survey = Survey::model()->findByPk($iSurveyID);
        $aData = [];
        $aData['sidemenu']['state'] = false;
        $aData['sidemenu']['questiongroups'] = true;

        $aData['sid'] = $iSurveyID;
        $aData['surveyid'] = $iSurveyID; // todo duplication needed for survey_common_action
        $aData['gid'] = $groupid;
        $aData['title_bar']['title'] = $survey->currentLanguageSettings->surveyls_title . " (" . gT("ID") . ":" . $iSurveyID . ")";
        $aData['topbar']['rightButtons'] = $this->renderPartial(
            'partial/topbarBtns/importQuestionTopbarRight_view',
            [],
            true
        );

        $this->aData = $aData;
        $this->render(
            'importQuestion',
            [
                'gid' => $aData['gid'],
                'sid' => $aData['sid']
            ]
        );
    }

    /**
     * Import the Question
     */
    public function actionImport()
    {
        $iSurveyID = (int) App()->request->getPost('sid', 0);
        $gid = (int) App()->request->getPost('gid', 0);

        if (!Permission::model()->hasSurveyPermission($iSurveyID, 'surveycontent', 'import')) {
            App()->session['flashmessage'] = gT("We are sorry but you don't have permissions to do this.");
            /* Same redirect than importView */
            $this->redirect(['questionAdministration/listquestions/surveyid/' . $iSurveyID]);
        }

        $jumptoquestion = (bool)App()->request->getPost('jumptoquestion', 1);

        $oSurvey = Survey::model()->findByPk($iSurveyID);

        $aData = [];
        $aData['display']['menu_bars']['surveysummary'] = 'viewquestion';
        $aData['display']['menu_bars']['gid_action'] = 'viewgroup';

        $sFullFilepath = App()->getConfig('tempdir') . DIRECTORY_SEPARATOR . randomChars(20);
        $fatalerror = '';

        // Check file size and redirect on error
        $uploadValidator = new LimeSurvey\Models\Services\UploadValidator();
        $uploadValidator->redirectOnError('the_file', \Yii::app()->createUrl('questionAdministration/importView', array('surveyid' => $iSurveyID)));

        $sExtension = pathinfo((string) $_FILES['the_file']['name'], PATHINFO_EXTENSION);
        if (!@move_uploaded_file($_FILES['the_file']['tmp_name'], $sFullFilepath)) {
            $fatalerror = gT(
                "An error occurred uploading your file."
                    . " This may be caused by incorrect permissions for the application /tmp folder."
            ) . '<br>';
        }

        // validate that we have a SID and GID
        if (!$iSurveyID) {
            $fatalerror .= gT("No SID (Survey) has been provided. Cannot import question.");
        }

        if (!$gid) {
            $fatalerror .= gT("No GID (Group) has been provided. Cannot import question");
        }

        if ($fatalerror != '') {
            unlink($sFullFilepath);
            App()->setFlashMessage($fatalerror, 'error');
            $this->redirect(['questionAdministration/importView', 'surveyid' => $iSurveyID]);
            return;
        }

        // load import_helper and import the file
        App()->loadHelper('admin/import');
        $aImportResults = [];
        if (strtolower($sExtension) === 'lsq') {
            $aImportResults = XMLImportQuestion(
                $sFullFilepath,
                $iSurveyID,
                $gid,
                [
                    'autorename'      => App()->request->getPost('autorename') == '1',
                    'translinkfields' => App()->request->getPost('autorename') == '1'
                ]
            );
        } else {
            App()->setFlashMessage(gT('Unknown file extension'), 'error');
            $this->redirect(['questionAdministration/importView', 'surveyid' => $iSurveyID]);
            return;
        }

        fixLanguageConsistency($iSurveyID);

        if (isset($aImportResults['fatalerror'])) {
            App()->setFlashMessage($aImportResults['fatalerror'], 'error');
            $this->redirect(['questionAdministration/importView', 'surveyid' => $iSurveyID]);
            return;
        }

        // If there are warnings, we don't jump to the question.
        // We need to show the warnings to the user, and they may be too important
        // and/or too many to be shown in a flash message.
        if (!empty($aImportResults['importwarnings'])) {
            $jumptoquestion = false;
        }

        unlink($sFullFilepath);

        $aData['aImportResults'] = $aImportResults;
        $aData['sid'] = $iSurveyID;
        $aData['surveyid'] = $iSurveyID; // todo needed in function beforeRender in this controller
        $aData['gid'] = $gid;
        $aData['sExtension'] = $sExtension;

        if ($jumptoquestion) {
            App()->setFlashMessage(gT("Question imported successfully"), 'success');
            $this->redirect(
                App()->createUrl(
                    'questionAdministration/view/',
                    [
                        'surveyid' => $iSurveyID,
                        'gid'      => $gid,
                        'qid'      => $aImportResults['newqid']
                    ]
                )
            );
            return;
        }

        $aData['sidemenu']['state'] = false; // todo ignored by sidebar.vue
        $aData['sidemenu']['landOnSideMenuTab'] = 'structure';
        $aData['title_bar']['title'] = $oSurvey->defaultlanguage->surveyls_title . " (" . gT("ID") . ":" . $iSurveyID . ")";

        $this->aData = $aData;
        $this->render(
            'import',
            [
                'aImportResults' => $aData['aImportResults'],
                'sExtension'     => $aData['sExtension'],
                'sid'            => $aData['sid'],
                'gid'            => $aData['gid']
            ]
        );
    }

    /**
     * Load edit default values of a question screen
     *
     * @access public
     * @param int $surveyid
     * @param int $gid
     * @param int $qid
     * @return void
     */
    public function actionEditdefaultvalues($surveyid, $gid, $qid)
    {
        if (!Permission::model()->hasSurveyPermission($surveyid, 'surveycontent', 'update')) {
            App()->user->setFlash('error', gT("Access denied"));
            $this->redirect(App()->request->urlReferrer);
        }
        $iSurveyID = (int)$surveyid;
        $gid = (int)$gid;
        $qid = (int)$qid;
        $oQuestion = Question::model()->findByAttributes(['qid' => $qid, 'gid' => $gid,]);
        // $aQuestionTypeMetadata = QuestionType::modelsAttributes();  this is old!
        // TODO: $questionMetaData should be $questionThemeSettings
        $questionMetaData = QuestionTheme::findQuestionMetaData($oQuestion->type)['settings'];
        $oSurvey = Survey::model()->findByPk($iSurveyID);

        $oDefaultValues = self::getDefaultValues($iSurveyID, $gid, $qid);

        $aData = [
            'oQuestion'    => $oQuestion,
            'qid'          => $qid,
            'sid'          => $iSurveyID,
            'surveyid'     => $iSurveyID, // todo needed in beforeRender
            'langopts'     => $oDefaultValues,
            'questionrow'  => $oQuestion->attributes,
            'gid'          => $gid,
            'questionMetaData' => $questionMetaData
            //'qtproperties' => $aQuestionTypeMetadata,
        ];
        $aData['oSurvey'] = $oSurvey;
        $aData['title_bar']['title'] = $oSurvey->currentLanguageSettings->surveyls_title . " (" . gT("ID") . ":" . $iSurveyID . ")";
        $aData['questiongroupbar']['savebutton']['form'] = 'frmeditgroup';
        $this->createUrl(
            "questionAdministration/view",
            ["surveyid" => $iSurveyID, "gid" => $gid, "qid" => $qid]
        );
        $aData['questiongroupbar']['closebutton']['url'] = $this->createUrl(
            "questionAdministration/view",
            ["surveyid" => $iSurveyID, "gid" => $gid, "qid" => $qid]
        );
        $aData['questiongroupbar']['saveandclosebutton']['form'] = 'frmeditgroup';
        $aData['display']['menu_bars']['surveysummary'] = 'editdefaultvalues';
        $aData['display']['menu_bars']['qid_action'] = 'editdefaultvalues';
        $aData['sidemenu']['state'] = false;
        $aData['sidemenu']['explorer']['state'] = true;
        $aData['sidemenu']['explorer']['gid'] = $gid ?? false;
        $aData['sidemenu']['explorer']['qid'] = $qid ?? false;
        $aData['sidemenu']['landOnSideMenuTab'] = 'structure';

        $aData['showSaveButton'] = true;
        $aData['showSaveAndCloseButton'] = true;
        $aData['showWhiteCloseButton'] = true;
        $aData['closeUrl'] = Yii::app()->createUrl(
            'questionAdministration/view/',
            [
                'surveyid' => $oQuestion->sid,
                'gid' => $oQuestion->gid,
                'qid' => $oQuestion->qid,
                'landOnSideMenuTab' => 'structure'
            ]
        );
        $aData['hasUpdatePermission'] = Permission::model()->hasSurveyPermission(
            $iSurveyID,
            'surveycontent',
            'update'
        ) ? '' : 'disabled="disabled" readonly="readonly"';

        $topbarData = TopbarConfiguration::getQuestionTopbarData($iSurveyID);
        $topbarData = array_merge($topbarData, $aData);
        $aData['topbar']['middleButtons'] = $this->renderPartial(
            'partial/topbarBtns/editQuestionTopbarLeft_view',
            $topbarData,
            true
        );
        $aData['topbar']['rightButtons'] = $this->renderPartial(
            '/surveyAdministration/partial/topbar/surveyTopbarRight_view',
            $topbarData,
            true
        );

        $this->aData = $aData;
        $this->render('editdefaultvalues', $aData);
    }

    /**
     * Delete multiple questions.
     * Called by ajax from question list.
     * Permission check is done by questions::delete()
     *
     * @return void
     * @throws CException
     */
    public function actionDeleteMultiple()
    {
        $aQids = json_decode(Yii::app()->request->getPost('sItems', ''));
        $aResults = [];

        foreach ($aQids as $iQid) {
            $oQuestion = Question::model()->with('questionl10ns')->findByPk($iQid);
            $oSurvey = Survey::model()->findByPk($oQuestion->sid);
            $sBaseLanguage = $oSurvey->language;

            if (is_object($oQuestion)) {
                $aResults[$iQid]['title'] = viewHelper::flatEllipsizeText(
                    $oQuestion->questionl10ns[$sBaseLanguage]->question,
                    true,
                    0
                );
                $result = $this->actionDelete($iQid, true);
                $aResults[$iQid]['result'] = $result['status'];
            }
        }

        $this->renderPartial(
            'ext.admin.survey.ListSurveysWidget.views.massive_actions._action_results',
            ['aResults' => $aResults, 'successLabel' => gT('Deleted')]
        );
    }

    /**
     * Function responsible for deleting a question.
     *
     * @access public
     * @param int $qid
     * @param bool $massAction
     * @param string $redirectTo 'questionlist' or 'groupoverview' or empty
     * @throws CDbException
     * @throws CHttpException
     */
    public function actionDelete($qid = null, $massAction = false, $redirectTo = null)
    {
        if (!Yii::app()->getRequest()->isPostRequest) {
            throw new CHttpException(405, gT('Invalid action'));
        }
        if (is_null($qid)) {
            $qid = Yii::app()->getRequest()->getPost('qid');
        }

        // @todo: request should specify the survey ID of the question to be deleted
        // - survey ID is verified before deletion
        $oQuestion = Question::model()->findByPk($qid);
        $surveyid = $oQuestion->sid;

        if (empty($redirectTo)) {
            $redirectTo = Yii::app()->getRequest()->getPost('redirectTo', 'questionlist');
        }
        if ($redirectTo == 'groupoverview') {
            $redirect = Yii::app()->createUrl(
                'questionGroupsAdministration/view/',
                [
                    'surveyid' => $surveyid,
                    'gid' => $oQuestion->gid,
                    'landOnSideMenuTab' => 'structure'
                ]
            );
        } else {
            $redirect = Yii::app()->createUrl(
                'questionAdministration/listQuestions/',
                [
                    'surveyid' => $surveyid,
                    'landOnSideMenuTab' => 'settings'
                ]
            );
        }

        $diContainer = \LimeSurvey\DI::getContainer();
        $questionAggregateService = $diContainer->get(
            QuestionAggregateService::class
        );

        try {
            $questionAggregateService->delete($surveyid, $qid);
        } catch (NotFoundException $e) {
            throw new CHttpException(404, gT('Invalid question id'));
        } catch (QuestionHasConditionsException $e) {
            $message = gT(
                'Question could not be deleted. '
                . 'There are conditions for other questions that rely '
                . 'on this question. '
                . 'You cannot delete this question until those conditions '
                . 'are removed.'
            );
            Yii::app()->setFlashMessage($message, 'error');
            $this->redirect($redirect);
        } catch (PermissionDeniedException $e) {
            throw new CHttpException(
                403,
                gT('You are not authorized to delete questions.')
            );
        }

        $message = gT(
            'Question was successfully deleted.'
        );

        if ($massAction) {
            return [
                'message' => $message,
                'status'  => true
            ];
        }
        if (Yii::app()->request->isAjaxRequest) {
            $this->renderJSON(
                [
                    'status'   => true,
                    'message'  => $message,
                    'redirect' => $redirect
                ]
            );
        }
        Yii::app()->session['flashmessage'] = $message;
        $this->redirect($redirect);
    }

    /**
     * Change the question group/order position of multiple questions
     *
     * @throws CException
     */
    public function actionSetMultipleQuestionGroup()
    {
        $aQids = json_decode(Yii::app()->request->getPost('sItems', '')); // List of question ids to update
        // New Group ID  (can be same group for a simple position change)
        $iGid = Yii::app()->request->getPost('group_gid');
        $iQuestionOrder = Yii::app()->request->getPost('questionposition'); // Wanted position

        $oQuestionGroup = QuestionGroup::model()->find('gid=:gid', [':gid' => $iGid]); // The New Group object
        $oSurvey = $oQuestionGroup->survey; // The Survey associated with this group

        if (Permission::model()->hasSurveyPermission($oSurvey->sid, 'surveycontent', 'update')) {
            // If survey is active it should not be possible to update
            if ($oSurvey->active == 'N') {
                if ($iQuestionOrder == "") {
                    // If asked "at the end"
                    $iQuestionOrder = (getMaxQuestionOrder($oQuestionGroup->gid));
                }
                self::changeMultipleQuestionPositionAndGroup($aQids, $iQuestionOrder, $oQuestionGroup);
            }
        }
    }

    /**
     * Change the Questions mandatory state
     */
    public function actionChangeMultipleQuestionMandatoryState()
    {
        $aQids = json_decode(Yii::app()->request->getPost('sItems', '')); // List of question ids to update
        $iSid = (int)Yii::app()->request->getPost('sid');
        $sMandatory = Yii::app()->request->getPost('mandatory', 'N');

        if (Permission::model()->hasSurveyPermission($iSid, 'surveycontent', 'update')) {
            self::setMultipleQuestionMandatoryState($aQids, $sMandatory, $iSid);
        }
    }

    /**
     * Change the "other" option for applicable question types
     */
    public function actionChangeMultipleQuestionOtherState()
    {
        $aQids = json_decode(Yii::app()->request->getPost('sItems', '')); // List of question ids to update
        $iSid = (int)Yii::app()->request->getPost('sid');
        $sOther = (Yii::app()->request->getPost('other') === '1') ? 'Y' : 'N';

        if (Permission::model()->hasSurveyPermission($iSid, 'surveycontent', 'update')) {
            self::setMultipleQuestionOtherState($aQids, $sOther, $iSid);
        }
    }

    /**
     * Change attributes for multiple questions
     * ajax request (this is a massive action for questionlists view)
     *
     */
    public function actionChangeMultipleQuestionAttributes()
    {
        $aQidsAndLang        = json_decode((string) $_POST['sItems']); // List of question ids to update
        $iSid                = Yii::app()->request->getPost('sid'); // The survey (for permission check)
        $aAttributesToUpdate = json_decode((string) $_POST['aAttributesToUpdate']); // The list of attributes to updates
        // TODO 1591979134468: this should be get from the question model
        $aValidQuestionTypes = str_split((string) $_POST['aValidQuestionTypes']); //The valid question types for those attributes

        // Calling th model
        QuestionAttribute::model()->setMultiple($iSid, $aQidsAndLang, $aAttributesToUpdate, $aValidQuestionTypes);
    }

    /**
     * Loads the possible Positions where a Question could be inserted to
     *
     * @param int $gid
     * @param string $classes
     * @return CWidget|mixed|void
     * @throws Exception
     */
    public function actionAjaxLoadPositionWidget($gid, $classes = '')
    {
        $oQuestionGroup = QuestionGroup::model()->find('gid=:gid', [':gid' => $gid]);
        if (
            is_a($oQuestionGroup, 'QuestionGroup') &&
            Permission::model()->hasSurveyPermission($oQuestionGroup->sid, 'surveycontent', 'read')
        ) {
            $aOptions = [
                'display'           => 'form_group',
                'oQuestionGroup'    => $oQuestionGroup,

            ];

            // TODO: Better solution: Hard-code allowed CSS classes.
            if ($classes != '' && $this->isValidCSSClass($classes)) {
                $aOptions['classes'] = $classes;
            }

            return App()->getController()->widget(
                'ext.admin.survey.question.PositionWidget.PositionWidget',
                $aOptions
            );
        }
        return;
    }

    /**
     * render selected items for massive action widget
     * @throws CException
     */

    public function actionRenderItemsSelected()
    {
        $aQids = json_decode(Yii::app()->request->getPost('$oCheckedItems', ''));
        $aResults     = [];
        $tableLabels  = [gT('Question ID'), gT('Question title'), gT('Status')];

        foreach ($aQids as $sQid) {
            $iQid        = (int)$sQid;
            $oQuestion      = Question::model()->with('questionl10ns')->findByPk($iQid);
            $oSurvey        = Survey::model()->findByPk($oQuestion->sid);
            $sBaseLanguage  = $oSurvey->language;

            if (is_object($oQuestion)) {
                $aResults[$iQid]['title'] = substr(
                    viewHelper::flatEllipsizeText(
                        $oQuestion->questionl10ns[$sBaseLanguage]->question,
                        true,
                        0
                    ),
                    0,
                    100
                );
                $aResults[$iQid]['result'] = 'selected';
            }
        }

        $this->renderPartial(
            'ext.admin.grid.MassiveActionsWidget.views._selected_items',
            [
                'aResults'     =>  $aResults,
                'successLabel' =>  gT('Selected'),
                'tableLabels'  =>  $tableLabels
            ]
        );
    }

    /**
     * Get HTML for general settings.
     * Called with Ajax after question type is selected.
     *
     * @param int $surveyId
     * @param string $questionType One-char string
     * @param string $questionTheme the question theme
     * @param int $questionId Null or 0 if new question is being created.
     * @return void
     */
    public function actionGetGeneralSettingsHTML(int $surveyId, string $questionType, string $questionTheme = null, $questionId = null)
    {
        if (empty($questionType)) {
            throw new CHttpException(405, 'Internal error: No question type');
        }
        // TODO: Difference between create and update permissions?
        if (!Permission::model()->hasSurveyPermission($surveyId, 'surveycontent', 'update')) {
            throw new CHttpException(403, gT('No permission'));
        }
        // NB: This works even when $questionId is null (get default question values).
        $question = $this->getQuestionObject($questionId, $questionType, null, $questionTheme);
        // NB: Only check permission when there is a question.
        if (!empty($question)) {
            // NB: Could happen if user manipulates request.
            if (!Permission::model()->hasSurveyPermission($question->sid, 'surveycontent', 'update')) {
                throw new CHttpException(403, gT('No permission'));
            }
        }
        $generalSettings = $this->getGeneralOptions(
            $question->qid,
            $questionType,
            $question->gid,
            $questionTheme
        );

        $questionThemeObject = QuestionTheme::model()->find('name=:name', array(':name' => $questionTheme));
        $this->renderPartial("generalSettings", [
            'generalSettings' => $generalSettings,
            'oSurvey' => Survey::model()->findByPk($surveyId),
            'question' => $question,
            'aQuestionTypeGroups' => $this->getQuestionTypeGroups(QuestionTheme::findAllQuestionMetaDataForSelector()),
            'questionTheme' => $questionThemeObject,
            'selectormodeclass' => $this->getSelectorModeClass(),
        ]);
    }

    /**
     * Copies a question
     *
     * @return void
     */
    public function actionCopyQuestion()
    {
        $aData = [];
        //load helpers
        Yii::app()->loadHelper('surveytranslator');
        Yii::app()->loadHelper('admin.htmleditor');

        //get params from request
        $surveyId = (int)Yii::app()->request->getParam('surveyId');
        $questionGroupId = (int)Yii::app()->request->getParam('questionGroupId');
        $questionIdToCopy = (int)Yii::app()->request->getParam('questionId');

        //permission check ...
        if (!Permission::model()->hasSurveyPermission($surveyId, 'surveycontent', 'create')) {
            Yii::app()->user->setFlash('error', gT("Access denied! You don't have permission to copy a question"));
            $this->redirect(Yii::app()->request->urlReferrer);
        }

        $oQuestion = Question::model()->findByAttributes([
            'sid' => $surveyId,
            'gid' => $questionGroupId,
            'qid' => $questionIdToCopy
        ]);
        if ($oQuestion === null) {
            Yii::app()->user->setFlash('error', gT("Question does not exist."));
            $this->redirect(Yii::app()->request->urlReferrer);
        }

        $oSurvey = Survey::model()->findByPk($surveyId);
        $oQuestionGroup = QuestionGroup::model()->find('gid=:gid', array(':gid' => $questionGroupId));
        $aData['surveyid'] = $surveyId; //this is important to load the correct layout (see beforeRender)

        // Array elements for frontend (topbar etc.)
        $aData['sidemenu']['landOnSideMenuTab'] = 'structure';
        $aData['title_bar']['title'] = $oSurvey->currentLanguageSettings->surveyls_title
            . " (" . gT("ID") . ":" . $surveyId . ")";
        $aData['closeUrl'] = Yii::app()->createUrl(
            'questionAdministration/view/',
            [
                'surveyid' => $oQuestion->sid,
                'gid' => $oQuestion->gid,
                'qid' => $oQuestion->qid,
                'landOnSideMenuTab' => 'structure'
            ]
        );

        $aData['topbar']['rightButtons'] = $this->renderPartial(
            'partial/topbarBtns/copyQuestionTopbarRight_view',
            [
                'closeUrl' => $aData['closeUrl']
            ],
            true
        );

        $aData['oSurvey'] = $oSurvey;
        $aData['oQuestionGroup'] = $oQuestionGroup;
        $aData['oQuestion'] = $oQuestion;

        //save the copy ...savecopy (submitbtn pressed ...)
        $savePressed = Yii::app()->request->getParam('savecopy');
        if (isset($savePressed) && $savePressed !== null) {
            $newTitle = Yii::app()->request->getParam('question')['title'];

            $newQuestionL10n = Yii::app()->request->getParam('questionI10N');
            $copyQuestionTextValues = [];
            if (!empty($newQuestionL10n)) {
                foreach ($newQuestionL10n as $lang => $texts) {
                    $questionText = $texts['question'] ?? '';
                    $questionHelp = $texts['help'] ?? '';
                    $copyQuestionTextValues[$lang] = new \LimeSurvey\Datavalueobjects\CopyQuestionTextValues($questionText, $questionHelp);
                }
            }

            $copyQuestionValues = new \LimeSurvey\Datavalueobjects\CopyQuestionValues();
            $copyQuestionValues->setOSurvey($oSurvey);
            $copyQuestionValues->setQuestionCode($newTitle);
            $copyQuestionValues->setQuestionGroupId((int)Yii::app()->request->getParam('gid'));
            $copyQuestionValues->setQuestiontoCopy($oQuestion);
            if (!empty($copyQuestionTextValues)) {
                $copyQuestionValues->setQuestionL10nData($copyQuestionTextValues);
            }
            $questionPosition = Yii::app()->request->getParam('questionposition');
            if ($questionPosition === '') { //this means "at the end"
                $questionPosition = -1; //integer indicator for "end"
            }
            //first ensure that all questions for the group have a question_order>0 and possibly set to this state
            Question::setQuestionOrderForGroup($questionGroupId);
            switch ((int)$questionPosition) {
                case -1: //at the end
                    $newQuestionPosition = Question::getHighestQuestionOrderNumberInGroup($questionGroupId) + 1;
                    break;
                case 0: //at beginning
                    //set all existing order numbers to +1, and the copied question to order number 1
                    Question::increaseAllOrderNumbersForGroup($questionGroupId);
                    $newQuestionPosition = 1;
                    break;
                default: //all other cases means after question X (the value coming from frontend is already correct)
                    Question::increaseAllOrderNumbersForGroup($questionGroupId, $questionPosition);
                    $newQuestionPosition = $questionPosition;
            }
            $copyQuestionValues->setQuestionPositionInGroup($newQuestionPosition);

            $copyQuestionService = new \LimeSurvey\Models\Services\CopyQuestion($copyQuestionValues);
            $copyOptions['copySubquestions'] = (int)Yii::app()->request->getParam('copysubquestions') === 1;
            $copyOptions['copyAnswerOptions'] = (int)Yii::app()->request->getParam('copyanswers') === 1;
            $copyOptions['copyDefaultAnswers'] = (int)Yii::app()->request->getParam('copydefaultanswers') === 1;
            $copyOptions['copySettings'] = (int)Yii::app()->request->getParam('copyattributes') === 1;
            if ($copyQuestionService->copyQuestion($copyOptions)) {
                App()->user->setFlash('success', gT("Saved copied question"));
                $newQuestion = $copyQuestionService->getNewCopiedQuestion();
                $this->redirect(
                    $this->createUrl(
                        'questionAdministration/view/',
                        array(
                            'surveyid' => $surveyId,
                            'gid' => $newQuestion->gid,
                            'qid' => $newQuestion->qid
                        )
                    )
                );
            } else {
                App()->user->setFlash('error', gT("Could not save copied question"));
            }
        }

        Yii::app()->getClientScript()->registerScript(
            'editorfiletype',
            "editorfiletype ='javascript';",
            CClientScript::POS_HEAD
        );
        App()->getClientScript()->registerScriptFile(
            App()->getConfig('adminscripts') . 'questionEditor.js',
            CClientScript::POS_END
        );
        PrepareEditorScript(true, $this);
        App()->session['FileManagerContext'] = "edit:survey:{$surveyId}";
        initKcfinder();
        // Add <input> with JSON as value, used by JavaScript.
        $aData['jsVariablesHtml'] = $this->renderPartial(
            '/admin/survey/Question/_subQuestionsAndAnwsersJsVariables',
            [
                'qid'               => $oQuestion->qid,
                'anslangs'          => $oQuestion->survey->allLanguages,
                // TODO
                'assessmentvisible' => false,
                'scalecount'        => $oQuestion->questionType->answerscales
            ],
            true
        );
        $this->aData = $aData;
        $this->render('copyQuestionForm', $aData);
    }

    /**
     * Get HTML for advanced settings.
     * Called with Ajax after question type is selected.
     *
     * @param int $surveyId
     * @param string $questionType One-char string
     * @param string $questionTheme
     * @param int $questionId Null or 0 if new question is being created.
     * @return void
     */
    public function actionGetAdvancedSettingsHTML(int $surveyId, string $questionType, string $questionTheme = null, $questionId = null)
    {
        if (empty($questionType)) {
            throw new CHttpException(405, 'Internal error: No question type');
        }
        // @todo Difference between create and update permissions?
        if (!Permission::model()->hasSurveyPermission($surveyId, 'surveycontent', 'update')) {
            throw new CHttpException(403, gT('No permission'));
        }
        Yii::app()->loadHelper("admin.htmleditor");
        // NB: This works even when $questionId is null (get default question values).
        $question = $this->getQuestionObject($questionId, $questionType, null, $questionTheme);
        if ($questionId) {
            // NB: Could happen if user manipulates request.
            if (!Permission::model()->hasSurveyPermission($question->sid, 'surveycontent', 'update')) {
                throw new CHttpException(403, gT('No permission'));
            }
        }
        $advancedSettings = $this->getAdvancedOptions(
            $question->qid,
            $questionType,
            $questionTheme
        );
        $this->renderPartial(
            "advancedSettings",
            [
                'advancedSettings'  => $advancedSettings,
                'question'         => $question,
                'oSurvey'           => $question->survey,
            ]
        );
    }

    /**
     * Get HTML for extra options (subquestions/answers).
     * Called with Ajax after question type is selected.
     *
     * @param int $surveyId
     * @param string $questionType One-char string
     * @param int $questionId Null or 0 if new question is being created.
     * @return void
     */
    public function actionGetExtraOptionsHTML(int $surveyId, string $questionType, $questionId = null)
    {
        if (empty($questionType)) {
            throw new CHttpException(405, 'Internal error: No question type');
        }
        // @todo Difference between create and update permissions?
        if (!Permission::model()->hasSurveyPermission($surveyId, 'surveycontent', 'update')) {
            throw new CHttpException(403, gT('No permission'));
        }
        Yii::app()->loadHelper("admin.htmleditor");
        // NB: This works even when $questionId is null (get default question values).
        $question = $this->getQuestionObject($questionId, $questionType);
        if ($questionId) {
            // NB: Could happen if user manipulates request.
            if (!Permission::model()->hasSurveyPermission($question->sid, 'surveycontent', 'update')) {
                throw new CHttpException(403, gT('No permission'));
            }
        }
        $this->renderPartial(
            "extraOptions",
            [
                'question'         => $question,
                'survey'           => $question->survey,
            ]
        );
    }

    /**
     * This function prepares the data for label set details
     *
     * @param int $lid
     * @return void
     */
    public function actionGetLabelsetDetails($lid)
    {
        $labelSet = LabelSet::model()->find('lid=:lid', array(':lid' => $lid));

        $result = [];
        $languages = [];

        if ($labelSet !== null) {
            $usedLanguages = explode(' ', (string) $labelSet->languages);

            foreach ($usedLanguages as $sLanguage) {
                $result[$sLanguage] = array_map(
                    function ($attribute) {
                        return \viewHelper::flatten($attribute);
                    },
                    $labelSet->attributes
                );
                foreach ($labelSet->labels as $oLabel) {
                    $result[$sLanguage]['labels'][] = $oLabel->getTranslated($sLanguage);
                };
                $languages[$sLanguage] = getLanguageNameFromCode($sLanguage, false);
            };
        }

        return Yii::app()->getController()->renderPartial(
            '/admin/super/_renderJson',
            array(
                'data' => [
                    'success'   => count($result) > 0,
                    'results'   => $result,
                    'languages' => $languages
                ],
            ),
            false,
            false
        );
    }

    /**
     * This function prepares the data for labelset
     *
     * @param int $sid
     * @param int $match
     * @return void
     */
    public function actionGetLabelsetPicker($sid, $match = 0, $language = null)
    {
        $criteria = new CDbCriteria();
        if ($match == 1 && !empty($language)) {
            $criteria->addCondition('languages LIKE :language');
            $criteria->params = [':language' => '%' . $language . '%'];
        }

        $labelSets = LabelSet::model()->findAll($criteria);
        // Create languagespecific array
        $result = [];
        foreach ($labelSets as $labelSet) {
            $result[] = array_map(
                function ($attribute) {
                    return \viewHelper::flatten($attribute);
                },
                $labelSet->attributes
            );
        }

        return Yii::app()->getController()->renderPartial(
            '/admin/super/_renderJson',
            array(
                'data' => [
                    'success'   => count($result) > 0,
                    'labelsets' => $result
                ],
            ),
            false,
            false
        );
    }

    /**
     * Check if label set can be replaced without problems
     *
     * @param int $lid
     * @param array $languages
     * @param boolean $checkAssessments
     * @return void
     */
    public function actionCheckLabel($lid, $languages, $checkAssessments)
    {
        $labelSet = LabelSet::model()->find('lid=:lid', array(':lid' => $lid));
        $label = Label::model()->count('lid=:lid AND assessment_value<>0', array(':lid' => $lid));
        $labelSetLangauges = explode(' ', (string) $labelSet->languages);
        $errorMessages = [];
        if ($checkAssessments && $label) {
            $errorMessages[] = gT('The existing label set has assessment values assigned.') . '<strong>' . gT('If you replace the label set the existing asssessment values will be lost.') . '</strong>';
        }
        if (count(array_diff($labelSetLangauges, $languages))) {
            $errorMessages[] = gT('The existing label set has different/more languages.') . '<strong>' . gT('If you replace the label set these translations will be lost.') . '</strong>';
        }
        if (count($errorMessages)) {
            foreach ($errorMessages as $errorMessage) {
                echo  $errorMessage . '<br>';
            }
            eT('Do you really want to continue?');
        } else {
            eT('You are about to replace an existing label set with the current answer options.');
            echo '<br>';
            eT('Continue?');
        }
    }

    /** @todo The following functions should be moved to model or a service class ++++++++++++++++++++++++++ */


    /**
     * Returns true if $class is a valid CSS class (alphanumeric + '-' and '_')
     *
     * @param string $class
     * @return bool
     */
    protected function isValidCSSClass($class)
    {
        $class = str_replace(['-', '_'], '', $class);
        return ctype_alnum($class);
    }

    /**
     * Set the other state for selected Questions
     *
     * @param array $aQids All question id's affected
     * @param string $sOther the "other" value 'Y' or 'N'
     * @param int $iSid survey ID
     */
    public static function setMultipleQuestionOtherState($aQids, $sOther, $iSid)
    {
        foreach ($aQids as $sQid) {
            $iQid = (int)$sQid;
            $oQuestion = Question::model()->findByPk(["qid" => $iQid], 'sid=:sid', [':sid' => $iSid]);
            // Only set the other state for question types that have this attribute (and no parent_qid)
            if ($oQuestion->getAllowOther()) {
                $oQuestion->other = $sOther;
                $oQuestion->save();
            }
        }
    }

    /**
     * Set the mandatory state for selected Questions
     *
     * @param array $aQids All question id's affected
     * @param string $sMandatory The mandatory va
     * @param int $iSid survey ID
     */
    public static function setMultipleQuestionMandatoryState($aQids, $sMandatory, $iSid)
    {
        foreach ($aQids as $sQid) {
            $iQid = (int)$sQid;
            $oQuestion = Question::model()->findByPk(["qid" => $iQid], 'sid=:sid', [':sid' => $iSid]);
            // These are the questions types that have no mandatory property - so ignore them
            if ($oQuestion->type != Question::QT_X_TEXT_DISPLAY && $oQuestion->type != Question::QT_VERTICAL_FILE_UPLOAD) {
                $oQuestion->mandatory = $sMandatory;
                $oQuestion->save();
            }
        }
    }

    /**
     * Change the question group/order position of multiple questions
     *
     * @param array $aQids all question id's affected
     * @param int $iQuestionOrder the desired position
     * @param QuestionGroup $oQuestionGroup the desired QuestionGroup
     * @throws CException
     */
    public static function changeMultipleQuestionPositionAndGroup($aQids, $iQuestionOrder, $oQuestionGroup)
    {
        $oTransaction = Yii::app()->db->beginTransaction();
        try {
            // Now, we push each question to the new question group
            // And update positions
            foreach ($aQids as $sQid) {
                // Question basic infos
                $iQid = (int)$sQid;
                $oQuestion = Question::model()->findByAttributes(['qid' => $iQid]); // Question object
                $oldGid = $oQuestion->gid; // The current GID of the question
                $oldOrder = $oQuestion->question_order; // Its current order

                // First, we update all the positions of the questions in the current group of the question
                // If they were after the question, we must decrease by one their position
                Question::model()->updateCounters(
                    ['question_order' => -1],
                    [
                        'condition' => 'gid=:gid AND question_order>=:order',
                        'params'    => [':gid' => $oldGid, ':order' => $oldOrder]
                    ]
                );

                // Then, we must update all the position of the question in the new group of the question
                // If they will be after the question, we must increase their position
                Question::model()->updateCounters(
                    ['question_order' => 1],
                    [
                        'condition' => 'gid=:gid AND question_order>=:order',
                        'params'    => [':gid' => $oQuestionGroup->gid, ':order' => $iQuestionOrder]
                    ]
                );

                // Then we move all the questions with the request QID (same question in different langagues)
                // to the new group, with the righ postion
                Question::model()->updateAll(
                    ['question_order' => $iQuestionOrder, 'gid' => $oQuestionGroup->gid],
                    'qid=:qid',
                    [':qid' => $iQid]
                );
                // Then we update its subquestions
                Question::model()->updateAll(
                    ['gid' => $oQuestionGroup->gid],
                    'parent_qid=:parent_qid',
                    [':parent_qid' => $iQid]
                );

                $iQuestionOrder++;
            }
            $oTransaction->commit();
        } catch (Exception $e) {
            $oTransaction->rollback();
        }
    }

    /**
     * Gets default value(s) for a question or subquestion from table defaultvalue_l10ns
     *
     * @param int $iSurveyID
     * @param int $gid
     * @param int $qid
     * @return array Array with defaultValues
     */
    public static function getDefaultValues(int $iSurveyID, int $gid, int $qid)
    {
        $aDefaultValues = [];
        $oQuestion = Question::model()->findByAttributes(['qid' => $qid, 'gid' => $gid,]);
        $aQuestionAttributes = $oQuestion->attributes;
        $aQuestionTypeMetadata = QuestionType::modelsAttributes();
        $oSurvey = Survey::model()->findByPk($iSurveyID);

        foreach ($oSurvey->allLanguages as $language) {
            $aDefaultValues[$language] = [];
            $aDefaultValues[$language][$aQuestionAttributes['type']] = [];

            // If there are answerscales
            if ($aQuestionTypeMetadata[$aQuestionAttributes['type']]['answerscales'] > 0) {
                for ($scale_id = 0; $scale_id < $aQuestionTypeMetadata[$aQuestionAttributes['type']]['answerscales']; $scale_id++) {
                    $aDefaultValues[$language][$aQuestionAttributes['type']][$scale_id] = [];

                    $defaultvalue = DefaultValue::model()->with('defaultvaluel10ns')->find(
                        'specialtype = :specialtype AND qid = :qid AND scale_id = :scale_id AND defaultvaluel10ns.language =:language',
                        [
                            ':specialtype' => '',
                            ':qid'         => $qid,
                            ':scale_id'    => $scale_id,
                            ':language'    => $language,
                        ]
                    );
                    $defaultvalue = !empty($defaultvalue->defaultvaluel10ns) && array_key_exists(
                        $language,
                        $defaultvalue->defaultvaluel10ns
                    ) ? $defaultvalue->defaultvaluel10ns[$language]->defaultvalue : null;
                    $aDefaultValues[$language][$aQuestionAttributes['type']][$scale_id]['defaultvalue'] = $defaultvalue;

                    $answerresult = Answer::model()->with('answerl10ns')->findAll(
                        'qid = :qid AND answerl10ns.language = :language',
                        [
                            ':qid'      => $qid,
                            ':language' => $language
                        ]
                    );
                    $aDefaultValues[$language][$aQuestionAttributes['type']][$scale_id]['answers'] = $answerresult;

                    if ($aQuestionAttributes['other'] === 'Y') {
                        $defaultvalue = DefaultValue::model()->with('defaultvaluel10ns')->find(
                            'specialtype = :specialtype AND qid = :qid AND scale_id = :scale_id AND defaultvaluel10ns.language =:language',
                            [
                                ':specialtype' => 'other',
                                ':qid'         => $qid,
                                ':scale_id'    => $scale_id,
                                ':language'    => $language,
                            ]
                        );
                        $defaultvalue = !empty($defaultvalue->defaultvaluel10ns) && array_key_exists(
                            $language,
                            $defaultvalue->defaultvaluel10ns
                        ) ? $defaultvalue->defaultvaluel10ns[$language]->defaultvalue : null;
                        $aDefaultValues[$language][$aQuestionAttributes['type']]['Ydefaultvalue'] = $defaultvalue;
                    }
                }
            }

            // If there are subquestions and no answerscales
            if (
                $aQuestionTypeMetadata[$aQuestionAttributes['type']]['answerscales'] == 0 &&
                $aQuestionTypeMetadata[$aQuestionAttributes['type']]['subquestions'] > 0
            ) {
                for ($scale_id = 0; $scale_id < $aQuestionTypeMetadata[$aQuestionAttributes['type']]['subquestions']; $scale_id++) {
                    $aDefaultValues[$language][$aQuestionAttributes['type']][$scale_id] = [];

                    $sqresult = Question::model()
                        ->with('questionl10ns')
                        ->findAll(
                            'sid = :sid AND gid = :gid AND parent_qid = :parent_qid AND scale_id = :scale_id AND questionl10ns.language =:language',
                            [
                                ':sid'        => $iSurveyID,
                                ':gid'        => $gid,
                                ':parent_qid' => $qid,
                                ':scale_id'   => 0,
                                ':language'   => $language
                            ]
                        );

                    $aDefaultValues[$language][$aQuestionAttributes['type']][$scale_id]['sqresult'] = [];

                    $options = [];
                    if ($aQuestionAttributes['type'] == Question::QT_M_MULTIPLE_CHOICE || $aQuestionAttributes['type'] == Question::QT_P_MULTIPLE_CHOICE_WITH_COMMENTS) {
                        $options = ['' => gT('(No default value)'), 'Y' => gT('Checked')];
                    }

                    foreach ($sqresult as $aSubquestion) {
                        $defaultvalue = DefaultValue::model()
                            ->with('defaultvaluel10ns')
                            ->find(
                                'specialtype = :specialtype AND qid = :qid AND sqid = :sqid AND scale_id = :scale_id AND defaultvaluel10ns.language =:language',
                                [
                                    ':specialtype' => '',
                                    ':qid'         => $qid,
                                    ':sqid'        => $aSubquestion['qid'],
                                    ':scale_id'    => $scale_id,
                                    ':language'    => $language
                                ]
                            );
                        $defaultvalue = !empty($defaultvalue->defaultvaluel10ns) && array_key_exists(
                            $language,
                            $defaultvalue->defaultvaluel10ns
                        ) ? $defaultvalue->defaultvaluel10ns[$language]->defaultvalue : null;

                        $question = $aSubquestion->questionl10ns[$language]->question;
                        $aSubquestion = $aSubquestion->attributes;
                        $aSubquestion['question'] = $question;
                        $aSubquestion['defaultvalue'] = $defaultvalue;
                        $aSubquestion['options'] = $options;

                        $aDefaultValues[$language][$aQuestionAttributes['type']][$scale_id]['sqresult'][] = $aSubquestion;
                    }
                }
            }
            if (
                $aQuestionTypeMetadata[$aQuestionAttributes['type']]['answerscales'] == 0 &&
                $aQuestionTypeMetadata[$aQuestionAttributes['type']]['subquestions'] == 0
            ) {
                $defaultvalue = DefaultValue::model()
                    ->with('defaultvaluel10ns')
                    ->find(
                        'specialtype = :specialtype AND qid = :qid AND scale_id = :scale_id AND defaultvaluel10ns.language =:language',
                        [
                            ':specialtype' => '',
                            ':qid'         => $qid,
                            ':scale_id'    => 0,
                            ':language'    => $language,
                        ]
                    );
                $aDefaultValues[$language][$aQuestionAttributes['type']][0] = !empty($defaultvalue->defaultvaluel10ns) && array_key_exists(
                    $language,
                    $defaultvalue->defaultvaluel10ns
                ) ? $defaultvalue->defaultvaluel10ns[$language]->defaultvalue : null;
            }
        }

        return $aDefaultValues;
    }

    /**
     * Creates a question object
     * This is either an instance of the placeholder model QuestionCreate for new questions,
     * or of Question for already existing ones
     *
     * todo: this should be moved to model ...
     *
     * @param int $iQuestionId
     * @param string $sQuestionType
     * @param int $gid
     * @return Question
     * @throws CException
     */
    private function getQuestionObject($iQuestionId = null, $sQuestionType = null, $gid = null, $questionThemeName = null)
    {
        //todo: this should be done in the action directly
        $iSurveyId = App()->request->getParam('sid') ??
            App()->request->getParam('surveyid') ??
            App()->request->getParam('surveyId');
        /** @var Question|null */
        $oQuestion = Question::model()->findByPk($iQuestionId);

        if (empty($oQuestion)) {
            $oQuestion = QuestionCreate::getInstance($iSurveyId, $sQuestionType, $questionThemeName);
        }

        if ($sQuestionType != null) {
            $oQuestion->type = $sQuestionType;
        }

        if ($questionThemeName != null) {
            $oQuestion->question_theme_name = $questionThemeName;
        }

        if ($gid != null) {
            $oQuestion->gid = $gid;
        }

        return $oQuestion;
    }

    /**
     * @todo document me
     *
     * @param int $iQuestionId
     * @param string $sQuestionType
     * @param int $gid
     * @param string $questionThemeName
     *
     * @return void|array
     * @throws CException
     */
    private function getGeneralOptions(
        $iQuestionId = null,
        $sQuestionType = null,
        $gid = null,
        $questionThemeName = null
    ) {
        $oQuestion = $this->getQuestionObject($iQuestionId, $sQuestionType, $gid, $questionThemeName);
        $result = $oQuestion
            ->getDataSetObject()
            ->getGeneralSettingsArray($oQuestion->qid, $sQuestionType, null, $questionThemeName);
        return $result;
    }

    /**
     * @todo document me.
     * @todo move this function somewhere else, this should not be part of controller ... (e.g. model)
     *
     * @param Question $oQuestion
     * @return array
     */
    private function getCompiledQuestionData(&$oQuestion)
    {
        LimeExpressionManager::StartProcessingPage(false, true);
        $aQuestionDefinition = array_merge($oQuestion->attributes, ['typeInformation' => $oQuestion->questionType]);
        $oQuestionGroup = QuestionGroup::model()->findByPk($oQuestion->gid);
        $aQuestionGroupDefinition = array_merge($oQuestionGroup->attributes, $oQuestionGroup->questiongroupl10ns);

        $aScaledSubquestions = $oQuestion->getOrderedSubQuestions();
        foreach ($aScaledSubquestions as $scaleId => $aSubquestions) {
            $aScaledSubquestions[$scaleId] = array_map(
                function ($oSubQuestion) {
                    return array_merge($oSubQuestion->attributes, $oSubQuestion->questionl10ns);
                },
                $aSubquestions
            );
        }

        $aScaledAnswerOptions = $oQuestion->getOrderedAnswers();
        foreach ($aScaledAnswerOptions as $scaleId => $aAnswerOptions) {
            $aScaledAnswerOptions[$scaleId] = array_map(
                function ($oAnswerOption) {
                    return array_merge($oAnswerOption->attributes, $oAnswerOption->answerl10ns);
                },
                $aAnswerOptions
            );
        }
        $aReplacementData = [];
        $questioni10N = [];
        foreach ($oQuestion->questionl10ns as $lng => $oQuestionI10N) {
            $questioni10N[$lng] = $oQuestionI10N->attributes;

            templatereplace(
                $oQuestionI10N->question,
                [],
                $aReplacementData,
                'Unspecified',
                false,
                $oQuestion->qid
            );

            $questioni10N[$lng]['question_expression'] = viewHelper::stripTagsEM(
                LimeExpressionManager::GetLastPrettyPrintExpression()
            );

            templatereplace($oQuestionI10N->help, [], $aReplacementData, 'Unspecified', false, $oQuestion->qid);
            $questioni10N[$lng]['help_expression'] = viewHelper::stripTagsEM(
                LimeExpressionManager::GetLastPrettyPrintExpression()
            );
        }
        LimeExpressionManager::FinishProcessingPage();
        return [
            'question'      => $aQuestionDefinition,
            'questiongroup' => $aQuestionGroupDefinition,
            'i10n'          => $questioni10N,
            'subquestions'  => $aScaledSubquestions,
            'answerOptions' => $aScaledAnswerOptions,
        ];
    }

    /**
     * It returns a preformatted array of advanced settings.
     *
     * @param int $iQuestionId
     * @param string $sQuestionType
     * @param string $sQuestionTheme
     * @return array
     * @throws CException
     * @throws Exception
     */
    private function getAdvancedOptions($iQuestionId = null, $sQuestionType = null, $sQuestionTheme = null)
    {
        //here we get a Question object (also if question is new --> QuestionCreate)
        $oQuestion = $this->getQuestionObject($iQuestionId, $sQuestionType, null, $sQuestionTheme);

        // Get the advanced settings array
        $advancedSettings = $oQuestion->getAdvancedSettingsWithValues();

        // Group the array in categories
        $questionAttributeHelper = new LimeSurvey\Models\Services\QuestionAttributeHelper();
        $advancedSettings = $questionAttributeHelper->groupAttributesByCategory($advancedSettings);

        // This category is "general setting".
        unset($advancedSettings['Attribute']);

        return $advancedSettings;
    }

    /**
     *
     * todo: this should be moved to model, not a controller function ...
     *
     * @param $oQuestion
     * @return array
     */
    private function getCompiledSurveyInfo($oQuestion)
    {
        $oSurvey = $oQuestion->survey;
        $aQuestionTitles = $oCommand = Yii::app()->db->createCommand()
            ->select('title')
            ->from('{{questions}}')
            ->where('sid=:sid and parent_qid=0')
            ->queryColumn([':sid' => $oSurvey->sid]);
        $isActive = $oSurvey->isActive;
        $questionCount = safecount($aQuestionTitles);
        $groupCount = safecount($oSurvey->groups);

        return [
            "aQuestionTitles" => $aQuestionTitles,
            "isActive"        => $isActive,
            "questionCount"   => $questionCount,
            "groupCount"      => $groupCount,
        ];
    }

    /**
     * Copies the default value(s) set for a question
     *
     * @param Question $oQuestion
     * @param integer $oldQid
     *
     * @return boolean
     * @throws CHttpException
     * @deprecated Functionality moved to CopyQuestion service.
     */
    private function copyDefaultAnswers($oQuestion, $oldQid)
    {
        if (empty($oldQid)) {
            return false;
        }

        $oOldDefaultValues = DefaultValue::model()->with('defaultvaluel10ns')->findAllByAttributes(['qid' => $oldQid]);

        $setApplied['defaultValues'] = array_reduce(
            $oOldDefaultValues,
            function ($collector, $oDefaultValue) use ($oQuestion) {
                $oNewDefaultValue = new DefaultValue();
                $oNewDefaultValue->setAttributes($oDefaultValue->attributes, false);
                $oNewDefaultValue->dvid = null;
                $oNewDefaultValue->qid = $oQuestion->qid;

                if (!$oNewDefaultValue->save()) {
                    throw new CHttpException(
                        500,
                        "Could not save default values. ERRORS:"
                            . print_r($oQuestion->getErrors(), true)
                    );
                }

                foreach ($oDefaultValue->defaultvaluel10ns as $oDefaultValueL10n) {
                    $oNewDefaultValueL10n = new DefaultValueL10n();
                    $oNewDefaultValueL10n->setAttributes($oDefaultValueL10n->attributes, false);
                    $oNewDefaultValueL10n->id = null;
                    $oNewDefaultValueL10n->dvid = $oNewDefaultValue->dvid;
                    if (!$oNewDefaultValueL10n->save()) {
                        throw new CHttpException(
                            500,
                            "Could not save default value I10Ns. ERRORS:"
                                . print_r($oQuestion->getErrors(), true)
                        );
                    }
                }

                return true;
            },
            true
        );
        return true;
    }

    /**
     * @param QuestionTheme[] $questionThemes Question theme List
     * @return array
     * @todo Move to PreviewModalWidget?
     */
    private function getQuestionTypeGroups(array $questionThemes)
    {
        $aQuestionTypeGroups = [];

        uasort($questionThemes, "questionTitleSort");
        foreach ($questionThemes as $questionTheme) {
            $htmlReadyGroup = str_replace(' ', '_', strtolower((string) $questionTheme->group));
            if (!isset($aQuestionTypeGroups[$htmlReadyGroup])) {
                $aQuestionTypeGroups[$htmlReadyGroup] = array(
                    'questionGroupName' => $questionTheme->group
                );
            }
            $imageName = $questionTheme->question_type;
            if ($imageName == ":") {
                $imageName = "COLON";
            } elseif ($imageName == "|") {
                $imageName = "PIPE";
            } elseif ($imageName == "*") {
                $imageName = "EQUATION";
            }
            $questionThemeData = [];
            $questionThemeData['title'] = $questionTheme->title;
            $questionThemeData['name'] = $questionTheme->name;
            $questionThemeData['type'] = $questionTheme->question_type;
            $questionThemeData['detailpage'] = '
                <div class="col-12 currentImageContainer">
                <img src="' . $questionTheme->image_path . '" />
                </div>';
            if ($imageName == 'S') {
                $questionThemeData['detailpage'] = '
                    <div class="col-12 currentImageContainer">
                    <img src="' . App()->getConfig('imageurl') . '/screenshots/' . $imageName . '.png" />
                    <img src="' . App()->getConfig('imageurl') . '/screenshots/' . $imageName . '2.png" />
                    </div>';
            }
            $aQuestionTypeGroups[$htmlReadyGroup]['questionTypes'][] = $questionThemeData;
        }
        return $aQuestionTypeGroups;
    }

    /**
     * Checks given answer code is unique.
     * @param string $code
     * @return bool
     */
    public function actionCheckAnswerCodeIsUnique(string $code): bool
    {
        $answer = Answer::model()->getAnswerFromCode($code);
        if ($answer->code !== $code || $answer === null) {
            $isValid = true;
        } else {
            $isValid = false;
        }
        return $isValid;
    }

    /**
     * Checks if given Sub Question Code is unique.
     * @param string $code
     * @return string
     */
    public function actionCheckSubQuestionCodeIsUnique(string $code): string
    {
        return '';
    }

    /**
     * @deprecated in 5.3.17
     * replaced by better name actionValidateQuestionTitle
     */
    public function actionCheckQuestionCodeUniqueness($sid, int $qid, string $code)
    {
        $this->actionCheckQuestionValidateTitle($sid, $qid, $code);
    }

    /**
     * Checks if given Question Code is unique.
     * Echo 'true' if code is unique, otherwise 'false'.
     *
     * @param int $sid Survey id
     * @param int $qid Question id
     * @param string $code Question code (title in db)
     * @return void
     */
    public function actionCheckQuestionValidateTitle($sid, int $qid, string $code)
    {
        $sid = (int) $sid;
        $qid = (int) $qid;
        if (!Permission::model()->hasSurveyPermission($sid, 'surveycontent', 'create')) {
            throw new CHttpException(403, gT('No permission'));
        }

        $survey = Survey::model()->findByPk($sid);
        if (empty($survey)) {
            throw new CHttpException(404, gT("Invalid survey ID"));
        }
        if ($qid) {
            $oQuestion = Question::model()->findByAttributes(['qid' => $qid, 'sid' => $sid]);
            if (empty($oQuestion)) {
                throw new CHttpException(404, gT("Invalid question id"));
            }
            if (!empty($oQuestion->parent_qid)) {
                throw new CHttpException(400, gT("Invalid question id"));
            }
            if ($oQuestion->sid != $sid) {
                throw new CHttpException(400, gT("Invalid question id"));
            }
        } else {
            $oQuestion = $this->getQuestionObject();
            $oQuestion->parent_qid = 0; // Unsure needed it, but we need it's a parent_qid=0
        }
        $oQuestion->title = $code;
        header('Content-Type: application/json');
        if (!$oQuestion->validate(['title'])) {
            echo json_encode(['message' => $oQuestion->getError('title')]);
        } else {
            echo json_encode(['message' => null]);
        }
        Yii::app()->end();
    }

    /**
     * Get HTML for question summary.
     * Called with Ajax after question is saved.
     *
     * @param int $questionId
     * @return void
     */
    public function actionGetSummaryHTML(int $questionId)
    {
        $question = Question::model()->findByPk($questionId);
        if (empty($question)) {
            throw new CHttpException(404, gT("Invalid question id"));
        }
        if (!Permission::model()->hasSurveyPermission($question->sid, 'surveycontent', 'read')) {
            throw new CHttpException(403, gT('No permission'));
        }

        // Use the question's theme if it exists, or a dummy theme if it doesn't
        /** @var QuestionTheme */
        $questionTheme = !empty($question->questionTheme) ? $question->questionTheme : QuestionTheme::getDummyInstance($question->type);

        /** @var array<string,array<mixed>> */
        $advancedSettings = $this->getAdvancedOptions($question->qid, $question->type, $question->question_theme_name);
        // Remove general settings from this array.
        unset($advancedSettings['Attribute']);

        $this->renderPartial(
            "questionSummary",
            [
                'survey' => $question->survey,
                'question' => $question,
                'questionTheme' => $questionTheme,
                'advancedSettings' => $advancedSettings,
                'overviewVisibility' => false,  // Hidden by default
            ]
        );
    }

    /**
     * Returns the selector mode class as string
     * @return string
     */
    private function getSelectorModeClass()
    {
        if (App()->session['questionselectormode'] !== 'default') {
            $selectorModeClass = App()->session['questionselectormode'];
        } else {
            $selectorModeClass = App()->getConfig('defaultquestionselectormode');
        }

        return $selectorModeClass;
    }
}