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/ResponsesController.php
<?php

/**
 * class ResponsesController
 **/
class ResponsesController extends LSBaseController
{
    /**
     * responses constructor.
     * @param $controller
     * @param $id
     */
    public function __construct($controller, $id)
    {
        parent::__construct($controller, $id);

        App()->loadHelper('surveytranslator');
    }

    /**
     * Set filters for all actions
     * @return string[]
     */
    public function filters()
    {
        return [
            'postOnly + delete, deleteSingle, deleteAttachments'
        ];
    }

    /**
     * Override default getActionParams
     * @return array
     */
    public function getActionParams()
    {
        return array_merge($_GET, $_POST);
    }

    /**
     * this is part of renderWrappedTemplate implement in old responses.php
     *
     * @param string $view
     * @return bool
     */
    public function beforeRender($view)
    {
        App()->getClientScript()->registerCssFile(App()->getConfig('publicstyleurl') . 'browse.css');

        $surveyId = (int)App()->request->getParam('surveyId');
        $oSurvey = Survey::model()->findByPk($surveyId);
        $this->aData['subaction'] = gT("Responses and statistics");
        $this->aData['display']['menu_bars']['browse'] = gT('Browse responses'); // browse is independent of the above
        $this->aData['title_bar']['title'] = gT('Browse responses') . ': ' . $oSurvey->currentLanguageSettings->surveyls_title;
        $this->aData['topBar']['type'] = 'responses';
        $this->layout = 'layout_questioneditor';

        return parent::beforeRender($view);
    }

    /**
     * @param int $surveyId
     * @param string $token
     */
    public function actionViewbytoken(int $surveyId, string $token): void
    {
        // Get Response ID from token
        $oResponse = SurveyDynamic::model($surveyId)->findByAttributes(['token' => $token]);
        if (!$oResponse) {
            App()->user->setFlash('error', gT("Sorry, this response was not found."));
            $this->redirect(["responses/browse/surveyId/{$surveyId}"]);
        } else {
            $this->redirect(["responses/view/", 'surveyId' => $surveyId, 'id' => $oResponse->id]);
        }
    }

    /**
     * View a single response as queXML PDF
     *
     * @param int $surveyId
     * @param int $id
     * @param string $browseLang
     * @throws CException
     * @throws CHttpException
     */
    public function actionViewquexmlpdf(int $surveyId, int $id, string $browseLang = ''): void
    {
        if (Permission::model()->hasSurveyPermission($surveyId, 'responses', 'read')) {
            $aData = $this->getData($surveyId, $id, $browseLang);
            $sBrowseLanguage = $aData['language'];
            Yii::import("application.libraries.admin.quexmlpdf", true);
            $quexmlpdf = new quexmlpdf();
            $quexmlpdf->applyGlobalSettings();
            // Setting the selected language for printout
            App()->setLanguage($sBrowseLanguage);
            $quexmlpdf->setLanguage($sBrowseLanguage);
            set_time_limit(120);
            App()->loadHelper('export');
            $quexml = quexml_export($surveyId, $sBrowseLanguage, $id);
            $quexmlpdf->create($quexmlpdf->createqueXML($quexml));
            $quexmlpdf->write_out("$surveyId-$id-queXML.pdf");
        } else {
            App()->user->setFlash('error', gT("You do not have permission to access this page."));
            $this->redirect(['surveyAdministration/view', 'surveyid' => $surveyId]);
        }
    }

    /**
     * View a single response in detail
     *
     * @param int $surveyId
     * @param int $id
     * @param string $browseLang
     * @throws CException
     * @throws CHttpException
     */
    public function actionView(int $surveyId, int $id, string $browseLang = ''): void
    {

        // logging for webserver when parameter is somehting like $surveyid=125<script ...
        if (!is_numeric(Yii::app()->request->getParam('surveyId'))) {
            throw new CHttpException(403, gT("Invalid survey ID"));
        }
        if (!is_numeric(Yii::app()->request->getParam('id'))) {
            throw new CHttpException(403, gT("Invalid response ID"));
        }
        $survey = Survey::model()->findByPk($surveyId);

        if (!Permission::model()->hasSurveyPermission($surveyId, 'responses', 'read')) {
            App()->user->setFlash('error', gT("You do not have permission to access this page."));
            $this->redirect(['surveyAdministration/view', 'surveyid' => $surveyId]);
            App()->end(); // More clear, uneeded.
        }
        /* TODO : Check if response still exist, after checking survey */
        $aData = $this->getData($surveyId, $id, $browseLang);
        $sBrowseLanguage = $aData['language'];

        extract($aData, EXTR_OVERWRITE);

        if ($id < 1) {
            $id = 1;
        }

        // Unless the response id is 0, getData() throws an exception if the response does not exist.
        // We just check it again here to be sure.
        $exist = SurveyDynamic::model($surveyId)->exist($id);
        if (!$exist) {
            throw new CHttpException(404, gT("Invalid response id."));
        }
        $next = SurveyDynamic::model($surveyId)->next($id, true);
        $previous = SurveyDynamic::model($surveyId)->previous($id, true);
        $aData['exist'] = $exist;
        $aData['next'] = $next;
        $aData['previous'] = $previous;
        $aData['id'] = $id;

        $fieldmap = createFieldMap($survey, 'full', false, false, $aData['language']);
        // just used to check if the token exists for the given response id before we create the real query
        $response = SurveyDynamic::model($surveyId)->find('id=:id', [':id' => $id]);
        // Boolean : show (or not) the token
        $bHaveToken = $survey->anonymized == "N"
            && tableExists('tokens_' . $surveyId)
            && isset($response->tokens);
        if (!Permission::model()->hasSurveyPermission($surveyId, 'tokens', 'read')) {
            // If not allowed to read: remove it
            unset($fieldmap['token']);
            $bHaveToken = false;
        }

        $oCriteria = new CDbCriteria();
        if ($bHaveToken) {
            $oCriteria = SurveyDynamic::model($surveyId)->addTokenCriteria($oCriteria);
        }
        $oCriteria->addCondition("id = {$id}");
        $iIdresult = SurveyDynamic::model($surveyId)->find($oCriteria);
        if ($bHaveToken) {
            $aResult = array_merge(
                $iIdresult->tokens->decrypt()->attributes,
                $iIdresult->decrypt()->attributes
            );
        } else {
            $aResult = $iIdresult->decrypt()->attributes;
        }

        //add token to top of list if survey is not private
        if ($bHaveToken) {
            $fnames[] = ["token", gT("Access code"), 'code' => 'token'];
            $fnames[] = ["firstname", gT("First name"), 'code' => 'firstname']; // or token:firstname ?
            $fnames[] = ["lastname", gT("Last name"), 'code' => 'lastname'];
            $fnames[] = ["email", gT("Email"), 'code' => 'email'];

            $customTokenAttributes = $survey->tokenAttributes;
            foreach ($customTokenAttributes as $attributeName => $tokenAttribute) {
                $tokenAttributeDescription = ($tokenAttribute['description'] != '') ? $tokenAttribute['description'] : $attributeName;
                $fnames[] = [$attributeName, $tokenAttributeDescription, 'code' => $attributeName];
            }
        }
        if ($survey->isDateStamp) {
            $fnames[] = ["submitdate", gT("Submission date"), gT("Completed"), "0", 'D', 'code' => 'submitdate'];
        }
        $fnames[] = ["completed", gT("Completed"), "0"];
        $qids = [];
        $fileUploadFields = [];

        foreach ($fieldmap as $field) {
            if ($field['fieldname'] == 'lastpage' || $field['fieldname'] == 'submitdate') {
                continue;
            }
            if ($field['type'] == 'interview_time') {
                continue;
            }
            if ($field['type'] == 'page_time') {
                continue;
            }
            if ($field['type'] == 'answer_time') {
                continue;
            }

            if ($field['type'] != Question::QT_VERTICAL_FILE_UPLOAD) {
                $fnames[] = [
                    $field['fieldname'],
                    viewHelper::getFieldText($field),
                    'code' => viewHelper::getFieldCode($field, ['LEMcompat' => true])
                ];
            } elseif ($field['aid'] !== 'filecount') {
                $qids[] = $field['qid'];
                $fileUploadFields[] = $field;
            } else {
                $fnames[] = [$field['fieldname'], gT("File count")];
            }
        }

        if (count($qids)) {
            $rawQuestions = Question::model()->findAllByPk($qids);
            $questions = [];
            foreach ($rawQuestions as $rawQuestion) {
                $questions[$rawQuestion->qid] = $rawQuestion;
            }
            foreach ($fileUploadFields as $field) {
                $filesInfo = json_decode_ls($aResult[$field['fieldname']]);
                if (empty($filesInfo)) {
                    continue;
                }
                $qidattributes = QuestionAttribute::model()->getQuestionAttributes($questions[$field['qid']]);

                $question = viewHelper::getFieldText($field);

                for ($i = 0; $i < count($filesInfo); $i++) {
                    $filenum = sprintf(gT("File %s"), $i + 1);
                    if ($qidattributes['show_title'] == 1) {
                        $fnames[] = [
                            $field['fieldname'],
                            "{$filenum} - {$question} (" . gT('Title') . ")",
                            'code'     => viewHelper::getFieldCode($field) . '(title)',
                            "type"     => Question::QT_VERTICAL_FILE_UPLOAD,
                            "metadata" => "title",
                            "index"    => $i
                        ];
                    }

                    if ($qidattributes['show_comment'] == 1) {
                        $fnames[] = [
                            $field['fieldname'],
                            "{$filenum} - {$question} (" . gT('Comment') . ")",
                            'code'     => viewHelper::getFieldCode($field) . '(comment)',
                            "type"     => Question::QT_VERTICAL_FILE_UPLOAD,
                            "metadata" => "comment",
                            "index"    => $i
                        ];
                    }

                    $fnames[] = [
                        $field['fieldname'],
                        "{$filenum} - {$question} (" . gT('File name') . ")",
                        'code'     => viewHelper::getFieldCode($field) . '(name)',
                        "type"     => "|",
                        "metadata" => "name",
                        "index"    => $i,
                        'qid'      => $field['qid']
                    ];
                    $fnames[] = [
                        $field['fieldname'],
                        "{$filenum} - {$question} (" . gT('File size') . ")",
                        'code'     => viewHelper::getFieldCode($field) . '(size)',
                        "type"     => "|",
                        "metadata" => "size",
                        "index"    => $i
                    ];
                }
            }
        }

        $nfncount = count($fnames) - 1;

        $oPurifier = new CHtmlPurifier();
        $id = $aResult['id'];
        $rlanguage = $aResult['startlanguage'];
        $aData['bHasFile'] = false;
        if (isset($rlanguage)) {
            $aData['rlanguage'] = $rlanguage;
        }
        $highlight = false;
        $aData['answers'] = [];
        for ($i = 0; $i < $nfncount + 1; $i++) {
            if ($fnames[$i][0] != 'completed' && is_null($aResult[$fnames[$i][0]])) {
                continue; // irrelevant, so don't show
            }
            $inserthighlight = '';
            if ($highlight) {
                $inserthighlight = "class='highlight'";
            }

            if ($fnames[$i][0] == 'completed') {
                if ($aResult['submitdate'] == null || $aResult['submitdate'] == "N") {
                    $answervalue = "N";
                } else {
                    $answervalue = "Y";
                }
            } elseif (isset($fnames[$i]['type']) && $fnames[$i]['type'] == Question::QT_VERTICAL_FILE_UPLOAD) {
                // File upload question type.
                $index = $fnames[$i]['index'];
                $metadata = $fnames[$i]['metadata'];
                $phparray = json_decode_ls($aResult[$fnames[$i][0]]);

                if (isset($phparray[$index])) {
                    switch ($metadata) {
                        case "size":
                            $answervalue = sprintf(gT("%s KB"), intval($phparray[$index][$metadata]));
                            break;
                        case "name":
                            $answervalue = CHtml::link(
                                htmlspecialchars(
                                    (string) $oPurifier->purify(rawurldecode((string) $phparray[$index][$metadata]))
                                ),
                                $this->createUrl(
                                    "responses/downloadfile",
                                    [
                                        "surveyId"    => $surveyId,
                                        "responseId" => $id,
                                        "qid"        => $fnames[$i]['qid'],
                                        "index"      => $index
                                    ]
                                )
                            );
                            break;
                        default:
                            $answervalue = htmlspecialchars(
                                strip_tags(
                                    stripJavaScript($phparray[$index][$metadata])
                                )
                            );
                    }
                    $aData['bHasFile'] = true;
                } else {
                    $answervalue = "";
                }
            } else {
                $answervalue = htmlspecialchars(
                    viewHelper::flatten(
                        stripJavaScript(
                            getExtendedAnswer(
                                $surveyId,
                                $fnames[$i][0],
                                $aResult[$fnames[$i][0]],
                                $sBrowseLanguage
                            )
                        )
                    ),
                    ENT_QUOTES
                );
            }
            $aData['inserthighlight'] = $inserthighlight;
            $aData['fnames'] = $fnames;
            $aData['answers'][] = [
                'answervalue' => $answervalue,
                'i' => $i
            ];
        }

        $aData['sidemenu']['state'] = false;
        // This resets the url on the close button to go to the upper view
        $aData['closeUrl'] = $this->createUrl("responses/browse/", ['surveyId' => $surveyId]);

        $topbarData = TopbarConfiguration::getResponsesTopbarData($survey->sid);
        $topbarData = array_merge($topbarData, $aData);
        $aData['topbar']['middleButtons'] = $this->renderPartial(
            'partial/topbarBtns/responseViewTopbarRight_view',
            $topbarData,
            true
        );

        $this->aData = $aData;
        $this->render('browseidrow_view', [
            'id'              => $aData['id'],
            'surveyid'        => $aData['surveyId'],
            'answers'         => $aData['answers'],
            'inserthighlight' => $aData['inserthighlight'],
            'fnames'          => $aData['fnames'],
        ]);
    }


    /**
     * Show responses for survey
     *
     * @param int $surveyId
     * @return void
     */
    public function actionBrowse(int $surveyId = 0, int $surveyid = 0): void
    {
        // Force it to accept `surveyid` as well, to maintain consistency with other menu entries.
        $surveyId = !empty($surveyId) ? $surveyId : (!empty($surveyid) ? $surveyid : null);
        // logging for webserver when parameter is somehting like $surveyid=125<script ...
        if (!is_numeric($surveyId)) {
            throw new CHttpException(403, gT("Invalid survey ID"));
        }
        $survey = Survey::model()->findByPk($surveyId);
        $displaymode = App()->request->getPost('displaymode', null);

        if ($displaymode !== null) {
            $this->setGridDisplay($displaymode);
        }

        if (Permission::model()->hasSurveyPermission($surveyId, 'responses', 'read')) {
            App()->getClientScript()->registerScriptFile(
                App()->getConfig('adminscripts') .
                    'listresponse.js',
                LSYii_ClientScript::POS_BEGIN
            );
            App()->getClientScript()->registerScriptFile(
                App()->getConfig('adminscripts') .
                    'tokens.js',
                LSYii_ClientScript::POS_BEGIN
            );

            // Basic data for the view
            $aData = $this->getData($surveyId);
            $aData['surveyid'] = $surveyId;
            $aData['sidemenu']['state'] = false;
            $aData['issuperadmin'] = Permission::model()->hasGlobalPermission('superadmin');
            $aData['hasUpload'] = hasFileUploadQuestion($surveyId);
            $aData['fieldmap'] = createFieldMap($survey, 'full', true, false, $aData['language']);
            $aData['dateformatdetails'] = getDateFormatData(App()->session['dateformat']);

            ////////////////////
            // Setting the grid

            // Basic variables
            $bHaveToken = $survey->anonymized == "N" && tableExists('tokens_' . $surveyId) && Permission::model()->hasSurveyPermission($surveyId, 'tokens', 'read'); // Boolean : show (or not) the token
            $model = SurveyDynamic::model($surveyId);
            $model->bEncryption = true;

            // Reset filters from stats
            if (App()->request->getParam('filters') == "reset") {
                App()->user->setState('sql_' . $surveyId, '');
            }

            // Page size
            if (App()->request->getParam('pageSize')) {
                App()->user->setState('pageSize', (int)App()->request->getParam('pageSize'));
            }

            // Model filters
            if (isset($_SESSION['survey_' . $surveyId])) {
                $sessionSurveyArray = App()->session->get('survey_' . $surveyId);
                $visibleColumns = $sessionSurveyArray['filteredColumns'] ?? null;
                if (!empty($visibleColumns)) {
                    $model->setAttributes($visibleColumns, false);
                }
            }
            // Using safe search on dynamic column names would be far too much complex.
            // So we pass over the safe validation and directly set attributes (second parameter of setAttributes to false).
            // see: http://www.yiiframework.com/wiki/161/understanding-safe-validation-rules/
            // see: http://www.yiiframework.com/doc/api/1.1/CModel#setAttributes-detail
            if (App()->request->getParam('SurveyDynamic')) {
                $model->setAttributes(App()->request->getParam('SurveyDynamic'), false);
            }

            // Virtual attributes filters
            // Filters on related tables need virtual filters attributes in main model (class variables)
            // Those virtual filters attributes are not set by the setAttributes, they must be set manually
            // @see: http://www.yiiframework.com/wiki/281/searching-and-sorting-by-related-model-in-cgridview/
            $aVirtualFilters = ['completed_filter', 'firstname_filter', 'lastname_filter', 'email_filter'];
            foreach ($aVirtualFilters as $sFilterName) {
                $aParam = App()->request->getParam('SurveyDynamic');
                if (!empty($aParam[$sFilterName])) {
                    $model->$sFilterName = $aParam[$sFilterName];
                }
            }

            // Sets which columns to filter
            $filteredColumns = !empty(isset($_SESSION['survey_' . $surveyId]['filteredColumns'])) ? $_SESSION['survey_' . $surveyId]['filteredColumns'] : null;
            $aData['filteredColumns'] = $filteredColumns;

            // rendering
            $aData['model'] = $model;
            $aData['bHaveToken'] = $bHaveToken;
            $aData['aDefaultColumns'] = $model->defaultColumns; // Some specific columns
            // Page size
            $aData['pageSize'] = App()->user->getState('pageSize', App()->params['defaultPageSize']);

            $topbarData = TopbarConfiguration::getResponsesTopbarData($survey->sid);
            $aData['topbar']['middleButtons'] = $this->renderPartial(
                'partial/topbarBtns/leftSideButtons',
                $topbarData,
                true
            );
            $aData['topbar']['rightButtons'] = $this->renderPartial(
                'partial/topbarBtns/rightSideButtons',
                $topbarData,
                true
            );
            // below codes are copied from above actionIndex method for summary page data
            $aData['num_total_answers'] = SurveyDynamic::model($surveyId)->count();
            $aData['num_completed_answers'] = SurveyDynamic::model($surveyId)->count('submitdate IS NOT NULL');
            // =============================================================================

            // these codes are copied from 'applicatioin\controllers\admin' for "saved but not submitted" table data
            // *** how it worked? admin/saved.php -> renderWrappedTemplate -> surveyCommonAction.php -> layout_insurvey
            $oSavedControlModel = SavedControl::model();
            $oSavedControlModel->sid = $survey->sid;

            // Filter state
            $aFilters = App()->request->getParam('SavedControl');
            if (!empty($aFilters)) {
                $oSavedControlModel->setAttributes($aFilters, false);
            }
            $aData['savedModel'] = $oSavedControlModel;
            if (App()->request->getPost('savedResponsesPageSize')) {
                App()->user->setState('savedResponsesPageSize', App()->request->getPost('savedResponsesPageSize'));
            }
            $aData['savedResponsesPageSize'] = App()->user->getState('savedResponsesPageSize', App()->params['defaultPageSize']);
            $aViewUrls[] = 'savedlist_view';
            // ===================================================

            $this->aData = $aData;

            $this->render('browseindex_view', [
                // summary table data
                'num_completed_answers' => $aData['num_completed_answers'],
                'num_total_answers'     => $aData['num_total_answers'],
                // response table data
                'surveyid' => $aData['surveyid'],
                'dateformatdetails' => $aData['dateformatdetails'],
                'model' => $aData['model'],
                'bHaveToken' => $aData['bHaveToken'],
                'language' => $aData['language'],
                'pageSize' => $aData['pageSize'],
                'fieldmap' => $aData['fieldmap'],
                'filteredColumns' => $aData['filteredColumns'],
                // saved but not submitted data
                'savedModel' => $aData['savedModel'],
                'savedResponsesPageSize' => $aData['savedResponsesPageSize'],

            ]);
        } else {
            App()->user->setFlash('error', gT("You do not have permission to access this page."));
            $this->redirect(['surveyAdministration/view', 'surveyid' => $surveyId]);
        }
    }

    /**
     * Saves the hidden columns for response browsing in the session
     * @access public
     * @param int $surveyId
     */
    public function actionSetFilteredColumns(int $surveyId): void
    {
        // logging for webserver when parameter is something like $surveyid=125<script ...
        if (!is_numeric(Yii::app()->request->getParam('surveyId'))) {
            throw new CHttpException(403, gT("Invalid survey ID"));
        }
        if (Permission::model()->hasSurveyPermission($surveyId, 'responses', 'read')) {
            $aFilteredColumns = [];
            $aColumns = (array)App()->request->getPost('columns');
            if (isset($aColumns)) {
                if (!empty($aColumns)) {
                    foreach ($aColumns as $sColumn) {
                        if (isset($sColumn)) {
                            $aFilteredColumns[] = $sColumn;
                        }
                    }
                    $_SESSION['survey_' . $surveyId]['filteredColumns'] = $aFilteredColumns;
                } else {
                    $_SESSION['survey_' . $surveyId]['filteredColumns'] = [];
                }
            }
        }
        $this->redirect(["responses/browse", "surveyId" => $surveyId]);
    }

    /**
     * Deletes multiple responses (massive action)
     *
     * @access public
     * @param int $surveyId
     * @return void
     * @throws CDbException
     * @throws CException
     * @throws CHttpException
     */
    public function actionDelete(int $surveyId): void
    {
        if (!is_numeric(Yii::app()->request->getParam('surveyId'))) {
            throw new CHttpException(403, gT("Invalid survey ID"));
        }
        if (!Permission::model()->hasSurveyPermission($surveyId, 'responses', 'delete')) {
            throw new CHttpException(403, gT("You do not have permission to access this page."));
        }
        if (!App()->getRequest()->isPostRequest) {
            throw new CHttpException(405, gT("Invalid action"));
        }
        Yii::import('application.helpers.admin.ajax_helper', true);

        $ResponseId = (App()->request->getPost('sItems') != '') ? json_decode(App()->request->getPost('sItems', '')) : json_decode(App()->request->getParam('sResponseId', ''), true);
        if (App()->request->getPost('modalTextArea') != '') {
            $ResponseId = explode(',', App()->request->getPost('modalTextArea', ''));
            foreach ($ResponseId as $key => $sResponseId) {
                $ResponseId[$key] = str_replace(' ', '', $sResponseId);
            }
        }

        $aResponseId = (is_array($ResponseId)) ? $ResponseId : [$ResponseId];
        $errors = 0;
        $timingErrors = 0;

        foreach ($aResponseId as $iResponseId) {
            $resultErrors = $this->deleteResponse($surveyId, $iResponseId);
            $errors += $resultErrors['numberOfErrors'];
            $timingErrors += $resultErrors['numberOfTimingErrors'];
        }

        if ($errors || $timingErrors) {
            $message = ($errors) ? ngT("A response was not deleted.|{n} responses were not deleted.", $errors) : "";
            $message .= ($timingErrors) ? ngT("A timing record was not deleted.|{n} timing records were not deleted.", $errors) : "";
            if (App()->getRequest()->isAjaxRequest) {
                ls\ajax\AjaxHelper::outputError($message);
            } else {
                App()->user->setFlash('error', $message);
                $this->redirect(["responses/browse", "surveyId" => $surveyId]);
            }
        }
        if (App()->getRequest()->isAjaxRequest) {
            ls\ajax\AjaxHelper::outputSuccess(gT('Response(s) deleted.'));
        }
        App()->user->setFlash('success', gT('Response(s) deleted.'));
        $this->redirect(["responses/browse", "surveyId" => $surveyId]);
    }

    /**
     * Deletes a single response and redirects to the gridview.
     *
     * @param int $surveyId -- the survey ID
     * @param int $responseId -- the response id to be deleted
     * @throws CDbException
     * @throws CHttpException
     */
    public function actionDeleteSingle(int $surveyId, int $responseId): void
    {
        if (!is_numeric(Yii::app()->request->getParam('surveyId'))) {
            throw new CHttpException(403, gT("Invalid survey ID"));
        }
        if (!is_numeric(Yii::app()->request->getParam('responseId'))) {
            throw new CHttpException(403, gT("Invalid response ID"));
        }
        if (!Permission::model()->hasSurveyPermission($surveyId, 'responses', 'delete')) {
            throw new CHttpException(403, gT("You do not have permission to access this page."));
        }

        $resultErrors = $this->deleteResponse($surveyId, $responseId);
        if ($resultErrors['numberOfErrors'] > 0 || $resultErrors['numberOfTimingErrors']) {
            $message = gT('Response could not be deleted');
            App()->user->setFlash('error', $message);
            $this->redirect(["responses/browse", "surveyId" => $surveyId]);
        }

        App()->user->setFlash('success', gT('Response deleted.'));
        $this->redirect(["responses/browse", "surveyId" => $surveyId]);
    }

    /**
     * Download individual file by response and filename
     *
     * @access public
     * @param int $surveyId : survey ID
     * @param int $responseId
     * @param int $qid
     * @param int $index
     * @return void
     * @throws CHttpException
     */
    public function actionDownloadfile(int $surveyId, int $responseId, int $qid, int $index): void
    {
        if (!is_numeric(Yii::app()->request->getParam('surveyId'))) {
            throw new CHttpException(403, gT("Invalid survey ID"));
        }
        if (!is_numeric(Yii::app()->request->getParam('responseId'))) {
            throw new CHttpException(403, gT("Invalid response ID"));
        }
        if (!is_numeric(Yii::app()->request->getParam('qid'))) {
            throw new CHttpException(403, gT("Invalid question ID"));
        }
        $oSurvey = Survey::model()->findByPk($surveyId);
        if (!$oSurvey->isActive) {
            App()->user->setFlash('error', gT('Sorry, this file was not found.'));
            $this->redirect(["surveyAdministration/view", "surveyid" => $surveyId]);
        }

        if (Permission::model()->hasSurveyPermission($surveyId, 'responses', 'read')) {
            $oResponse = Response::model($surveyId)->findByPk($responseId);
            if (is_null($oResponse)) {
                App()->user->setFlash('error', gT('Found no response with ID %d'), $responseId);
                $this->redirect(["responses/browse", "surveyId" => $surveyId]);
            }
            $aQuestionFiles = $oResponse->getFiles($qid);
            if (isset($aQuestionFiles[$index])) {
                $aFile = $aQuestionFiles[$index];
                // Real path check from here: https://stackoverflow.com/questions/4205141/preventing-directory-traversal-in-php-but-allowing-paths
                $sDir = Yii::app()->getConfig('uploaddir') . DIRECTORY_SEPARATOR . "surveys" . DIRECTORY_SEPARATOR . $surveyId . DIRECTORY_SEPARATOR . "files" . DIRECTORY_SEPARATOR;
                $sFileRealName = $sDir . $aFile['filename'];
                $sRealUserPath = get_absolute_path($sFileRealName);
                if ($sRealUserPath === false) {
                    throw new CHttpException(404, "File not found.");
                } elseif (strpos((string) $sRealUserPath, $sDir) !== 0) {
                    throw new CHttpException(403, "File cannot be accessed.");
                } else {
                    $mimeType = CFileHelper::getMimeType($sFileRealName, null, false);
                    if (is_null($mimeType)) {
                        $mimeType = "application/octet-stream";
                    }
                    @ob_clean();
                    header('Content-Description: File Transfer');
                    header('Content-Type: ' . $mimeType);
                    header('Content-Disposition: attachment; filename="' . sanitize_filename(rawurldecode((string) $aFile['name'])) . '"');
                    header('Content-Transfer-Encoding: binary');
                    header('Expires: 0');
                    header("Cache-Control: must-revalidate, no-store, no-cache");
                    header('Content-Length: ' . filesize($sFileRealName));
                    readfile($sFileRealName);
                    exit;
                }
            }
            App()->user->setFlash('error', gT('Sorry, this file was not found.'));
            $this->redirect(["responses/browse", "surveyId" => $surveyId]);
        } else {
            throw new CHttpException(403, gT("You do not have permission to access this page."));
        }
    }

    /**
     * Construct a zip files from a list of response
     *
     * @access public
     * @param int $surveyId : survey ID
     * @param string $responseIds : list of responses as string
     * @return void application/zip
     * @throws CException
     */
    public function actionDownloadfiles(int $surveyId, string $responseIds = ''): void
    {
        if (!is_numeric(Yii::app()->request->getParam('surveyId'))) {
            throw new CHttpException(403, gT("Invalid survey ID"));
        }
        if (Permission::model()->hasSurveyPermission($surveyId, 'responses', 'read')) {
            $oSurvey = Survey::model()->findByPk($surveyId);
            if (!$oSurvey->isActive) {
                App()->user->setFlash('error', gT('Sorry, this file was not found.'));
                $this->redirect(["surveyAdministration/view", "surveyid" => $surveyId]);
            }
            if (!$responseIds) {
                // No response id : get all survey files
                $oCriteria = new CDbCriteria();
                $oCriteria->select = "id";
                $oSurvey = SurveyDynamic::model($surveyId);
                $aResponseId = $oSurvey->getCommandBuilder()
                    ->createFindCommand($oSurvey->tableSchema, $oCriteria)
                    ->queryColumn();
            } else {
                $aResponseId = explode(",", $responseIds);
            }
            if (!empty($aResponseId)) {
                // Now, zip all the files in the filelist
                if (count($aResponseId) === 1) {
                    $zipfilename = "Files_for_survey_{$surveyId}_response_{$aResponseId[0]}.zip";
                } else {
                    $zipfilename = "Files_for_survey_{$surveyId}.zip";
                }
                $this->zipFiles($surveyId, $aResponseId, $zipfilename);
            } else {
                // No response : redirect to browse with a alert
                App()->user->setFlash('error', gT('The requested files do not exist on the server.'));
                $this->redirect(["responses/browse", "surveyId" => $surveyId]);
            }
        } else {
            throw new CHttpException(403, gT("You do not have permission to access this page."));
        }
    }

    /**
     * Delete all uploaded files for one response.
     *
     * @param int $surveyId
     * @param int|null $responseId
     * @return void
     * @throws CException
     * @throws CHttpException
     */
    public function actionDeleteAttachments(int $surveyId, int $responseId = null): void
    {
        if (!is_numeric(Yii::app()->request->getParam('surveyId'))) {
            throw new CHttpException(403, gT("Invalid survey ID"));
        }
        if (!Permission::model()->hasSurveyPermission($surveyId, 'responses', 'update')) {
            throw new CHttpException(403, gT("You do not have permission to access this page."));
        }
        $request = App()->request;
        if (!$request->isPostRequest) {
            throw new CHttpException(405, gT("Invalid action"));
        }

        $stringItems = json_decode($request->getPost('sItems', ''));
        // Cast all ids to int.
        $items = array_map(
            function ($id) {
                return (int)$id;
            },
            is_array($stringItems) ? $stringItems : []
        );
        $responseIds = $responseId !== null ? [$responseId] : $items;

        Yii::import('application.helpers.admin.ajax_helper', true);
        $allErrors = [];
        $allSuccess = 0;

        foreach ($responseIds as $responseIdLoop) {
            $response = Response::model($surveyId)->findByPk($responseIdLoop);
            if ($response !== null) {
                [$success, $errors] = $response->deleteFilesAndFilename();
                if (empty($errors)) {
                    $allSuccess += $success;
                } else {
                    // Could not delete all files.
                    $allErrors = array_merge($allErrors, $errors);
                }
            } else {
                $allErrors[] = sprintf(gT('Found no response with ID %d'), $responseIdLoop);
            }
        }
        if (!empty($allErrors)) {
            $message = gT('Error: Could not delete some files: ') . implode(', ', $allErrors);
            if ($request->isAjaxRequest) {
                ls\ajax\AjaxHelper::outputError(
                    $message
                );
                App()->end();
            }
            App()->user->setFlash('error', $message);
            $this->redirect(["responses/browse", "surveyId" => $surveyId]);
        }
        $message = sprintf(ngT('%d file deleted.|%d files deleted.', $allSuccess), $allSuccess);
        if ($request->isAjaxRequest) {
            ls\ajax\AjaxHelper::outputSuccess($message);
            App()->end();
        }
        App()->user->setFlash('success', $message);
        $this->redirect(["responses/browse", "surveyId" => $surveyId]);
    }

    /**
     * Time statistics for responses
     *
     * @param int $surveyId
     * @return void
     */
    public function actionTime(int $surveyId): void
    {
        $aData = $this->getData($surveyId);

        $aData['columns'] = [
            [
                'header'            => gT('ID'),
                'name'              => 'id',
                'value'             => '$data->id',
                'headerHtmlOptions' => ['class' => ''],
                'htmlOptions'       => ['class' => '']
            ],
            [
                'header' => gT('Total time'),
                'name'   => 'interviewtime',
                'value'  => '$data->interviewtime'
            ]
        ];

        $fields = createTimingsFieldMap($surveyId, 'full', true, false, $aData['language']);
        foreach ($fields as $fielddetails) {
            // headers for answer id and time data
            if ($fielddetails['type'] === 'id') {
                $fnames[] = [$fielddetails['fieldname'], $fielddetails['question']];
            }

            if ($fielddetails['type'] === 'interview_time') {
                $fnames[] = [$fielddetails['fieldname'], gT('Total time')];
            }

            if ($fielddetails['type'] === 'page_time') {
                $fnames[] = [$fielddetails['fieldname'], gT('Group') . ": " . $fielddetails['group_name']];
                $aData['columns'][] = [
                    'header' => gT('Group: ') . $fielddetails['group_name'],
                    'name'   => $fielddetails['fieldname']
                ];
            }

            if ($fielddetails['type'] === 'answer_time') {
                $fnames[] = [$fielddetails['fieldname'], gT('Question') . ": " . $fielddetails['title']];
                $aData['columns'][] = [
                    'header' => gT('Question: ') . $fielddetails['title'],
                    'name'   => $fielddetails['fieldname']
                ];
            }
        }
        $aData['columns'][] = [
            'name'              => 'actions',
            'type'              => 'raw',
            'header'            => gT("Action"),
            'headerHtmlOptions' => ['class' => 'ls-sticky-column'],
            'filterHtmlOptions' => ['class' => 'ls-sticky-column'],
            'htmlOptions'       => ['class' => 'ls-sticky-column']
        ];

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

        //interview Time statistics
        $aData['model'] = SurveyTimingDynamic::model($surveyId);

        $aData['pageSize'] = App()->user->getState('pageSize', Yii::app()->params['defaultPageSize']);
        $aData['statistics'] = SurveyTimingDynamic::model($surveyId)->statistics();
        $aData['num_total_answers'] = SurveyDynamic::model($surveyId)->count();
        $aData['num_completed_answers'] = SurveyDynamic::model($surveyId)->count('submitdate IS NOT NULL');

        //$aData['topBar']['name'] = 'baseTopbar_view';
        //$aData['topBar']['leftSideView'] = 'responsesTopbarLeft_view';

        $topbarData = TopbarConfiguration::getResponsesTopbarData($surveyId);
        $aData['topbar']['middleButtons'] = $this->renderPartial(
            'partial/topbarBtns/leftSideButtons',
            $topbarData,
            true
        );

        $this->aData = $aData;
        $this->render('browsetimerow_view', [
            'model'      => $aData['model'],
            'surveyId'  => $aData['surveyId'],
            'language'   => $aData['language'],
            'pageSize'   => $aData['pageSize'],
            'columns'    => $aData['columns'],
            'statistics' => $aData['statistics'],
        ]);
    }

    /**
     * Change the value of the max characters to elipsize headers/questions in response grid.
     * It's called via ajax request
     *
     * @param string $displaymode
     * @return void
     */
    public function setGridDisplay($displaymode): void
    {
        if ($displaymode === 'extended') {
            App()->user->setState('responsesGridSwitchDisplayState', 'extended');
            App()->user->setState('defaultEllipsizeHeaderValue', 1000);
            App()->user->setState('defaultEllipsizeQuestionValue', 1000);
        } else {
            App()->user->setState('responsesGridSwitchDisplayState', 'compact');
            App()->user->setState('defaultEllipsizeHeaderValue', App()->params['defaultEllipsizeHeaderValue']);
            App()->user->setState('defaultEllipsizeQuestionValue', App()->params['defaultEllipsizeQuestionValue']);
        }
    }

    /**
     * Supply an array with the responseIds and all files will be added to the zip
     * and it will be be spit out on success
     *
     * @param int $surveyId
     * @param array $responseId
     * @param string $zipfilename
     */
    private function zipFiles(int $surveyId, array $responseId, string $zipfilename): void
    {
        $tmpdir = App()->getConfig('uploaddir') . DIRECTORY_SEPARATOR . "surveys" . DIRECTORY_SEPARATOR . $surveyId . DIRECTORY_SEPARATOR . "files" . DIRECTORY_SEPARATOR;

        $filelist = [];
        $responses = Response::model($surveyId)->findAllByPk($responseId);
        $filecount = 0;
        foreach ($responses as $response) {
            foreach ($response->getFiles() as $fileInfo) {
                $filecount++;
                /*
                * Now add the file to the archive, prefix files with responseid_index to keep them
                * unique. This way we can have 234_1_image1.gif, 234_2_image1.gif as it could be
                * files from a different source with the same name.
                */
                if (file_exists($tmpdir . basename((string) $fileInfo['filename']))) {
                    $filelist[] = [
                        $tmpdir . basename((string) $fileInfo['filename']),
                        sprintf("%05s_%02s-%s_%02s-%s", $response->id, $filecount, $fileInfo['question']['title'], $fileInfo['index'], sanitize_filename(rawurldecode((string) $fileInfo['name'])))
                    ];
                }
            }
        }

        if (count($filelist) > 0) {
            $zip = new ZipArchive();
            $zip->open($tmpdir . $zipfilename, ZipArchive::CREATE);
            foreach ($filelist as $aFile) {
                $zip->addFile($aFile[0], $aFile[1]);
            }
            $zip->close();
            if (file_exists($tmpdir . '/' . $zipfilename)) {
                @ob_clean();
                header('Content-Description: File Transfer');
                header('Content-Type: application/zip, application/octet-stream');
                header('Content-Disposition: attachment; filename=' . basename($zipfilename));
                header('Content-Transfer-Encoding: binary');
                header('Expires: 0');
                header("Cache-Control: must-revalidate, no-store, no-cache");
                header('Content-Length: ' . filesize($tmpdir . "/" . $zipfilename));
                readfile($tmpdir . '/' . $zipfilename);
                unlink($tmpdir . '/' . $zipfilename);
                exit;
            }
        }
        // No files : redirect to browse with a alert
        App()->user->setFlash('error', gT("Sorry, there are no files for this response."));
        $this->redirect(["responses/browse", "surveyId" => $surveyId]);
    }

    /**
     * Used to get responses data for browse etc
     *
     * @param int|null $surveyId
     * @param int|null $responseId
     * @param string|null $language
     * @return array
     */
    private function getData(int $surveyId = null, int $responseId = null, string $language = null): array
    {
        if (!isset($surveyId)) {
            App()->setFlashMessage(gT("Invalid survey ID"), 'warning');
            $this->redirect(["dashboard/view"]);
        }

        $thissurvey = getSurveyInfo($surveyId);

        // 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($thissurvey['oSurvey']->language);
        LimeExpressionManager::SetSurveyId($surveyId);
        LimeExpressionManager::StartProcessingPage(false, true);

        if (!$thissurvey) {
            App()->setFlashMessage(gT("Invalid survey ID"), 'warning');
            $this->redirect(["dashboard/view"]);
        } elseif ($thissurvey['active'] !== 'Y') {
            App()->setFlashMessage(gT("This survey has not been activated. There are no results to browse."), 'warning');
            $this->redirect(["surveyAdministration/view/surveyid/{$surveyId}"]);
        }
        $aData = [];
        // Set the variables in an array
        $aData['surveyId'] = $aData['surveyid'] = $aData['iSurveyId'] = $surveyId;
        if (!empty($responseId)) {
            /* Check if exists  */
            if (empty(SurveyDynamic::model($surveyId)->findByPk($responseId))) {
                throw new CHttpException(404, gT("Invalid response id."));
            }
            $aData['iId'] = $responseId;
        }
        $aData['imageurl'] = App()->getConfig('imageurl');
        $aData['action'] = App()->request->getParam('action');
        $aData['all'] = App()->request->getParam('all');

        //OK. IF WE GOT THIS FAR, THEN THE SURVEY EXISTS AND IT IS ACTIVE, SO LETS GET TO WORK.
        if (!empty($language)) {
            $aData['language'] = $language;
            $aData['languagelist'] = $languagelist = Survey::model()->findByPk($surveyId)->additionalLanguages;
            $aData['languagelist'][] = Survey::model()->findByPk($surveyId)->language;
            if (!in_array($aData['language'], $languagelist)) {
                $aData['language'] = $thissurvey['language'];
            }
        } else {
            $aData['language'] = $thissurvey['language'];
        }

        $aData['qulanguage'] = Survey::model()->findByPk($surveyId)->language;

        $aData['surveyoptions'] = '';
        $aData['browseoutput'] = '';

        return $aData;
    }

    /**
     * Deletes a response
     *
     * @param $surveyId
     * @param $iResponseId
     * @return int[]
     * @throws CDbException
     */
    private function deleteResponse($surveyId, $iResponseId): array
    {
        $errors = 0;
        $timingErrors = 0;

        $beforeDataEntryDelete = new PluginEvent('beforeDataEntryDelete');
        $beforeDataEntryDelete->set('iSurveyID', $surveyId);
        $beforeDataEntryDelete->set('iResponseID', $iResponseId);
        App()->getPluginManager()->dispatchEvent($beforeDataEntryDelete);

        $response = Response::model($surveyId)->findByPk($iResponseId);
        if ($response) {
            $result = $response->delete(true);
            if (!$result) {
                ++$errors;
            }
        } else {
            ++$errors;
        }

        return ['numberOfErrors' => $errors, 'numberOfTimingErrors' => $timingErrors];
    }
}