<?php

/**
 * Twig view renderer, LimeSurvey overload
 *
 * Allow to run sandbox Configuration
 * Provide different render methods for different context:
 *
 * - render()            : for general use
 * - renderQuestion()    : to render a question. It checks if a question template view should be use, else core's view (used from qanda helper).
 * - convertTwigToHtml() : to render a string without any file (used from replacement helper)
 *
 * The only tricky point here is the path problematic (where should be searched the views to render?)
 * @see: http://twig.sensiolabs.org/doc/2.x/api.html#loaders
 *
 * @author Leonid Svyatov <leonid@svyatov.ru>
 * @author Alexander Makarov <sam@rmcreative.ru>
 * @link http://github.com/yiiext/twig-renderer
 * @link http://twig.sensiolabs.org
 *
 * @version 1.1.15
 */
class LSETwigViewRenderer extends ETwigViewRenderer
{
    /**
     * @var array Twig_Extension_Sandbox configuration
     */
    public $sandboxConfig = array();

    /**
     * @var Twig_Environment|null
     */
    private $_twig;

    /**
     * @var array Custom LS Users Extensions
     * Example: array('HelloWorld_Twig_Extension')
     */
    public $user_extensions = [];

    /**
     * @inheritdoc
     */
    function init()
    {
        parent::init();
        // Adding user custom extensions
        if (!empty($this->user_extensions)) {
            $this->addUserExtensions($this->user_extensions);
        }
    }

    /**
     * Main method to render a survey.
     * @param string $sLayout the name of the layout to render
     * @param array $aData the datas needed to fill the layout
     * @param boolean $bReturn if true, it will return the html string without
     *                         rendering the whole page. Useful for debugging, and used for Print Answers
     * @return mixed|string
     * @throws CException
     * @throws Throwable
     * @throws Twig_Error_Loader
     * @throws Twig_Error_Syntax
     * @throws WrongTemplateVersionException
     */
    public function renderTemplateFromFile($sLayout, $aData, $bReturn)
    {
        $oTemplate = Template::getLastInstance();
        $oLayoutTemplate = $this->getTemplateForView($sLayout, $oTemplate);
        if ($oLayoutTemplate) {
            $line       = file_get_contents($oLayoutTemplate->viewPath . $sLayout);
            $sHtml      = $this->convertTwigToHtml($line, $aData, $oTemplate);
            $sEmHiddenInputs = LimeExpressionManager::FinishProcessPublicPage(true);
            if ($sEmHiddenInputs) {
                $sHtml = str_replace(
                    "<!-- emScriptsAndHiddenInputs -->",
                    "<!-- emScriptsAndHiddenInputs updated -->\n" .
                    $sEmHiddenInputs,
                    $sHtml
                );
            }
            if ($bReturn) {
                return $sHtml;
            } else {
                $this->renderHtmlPage($sHtml, $oTemplate);
            }
        } else {
            $templateDbConf = Template::getTemplateConfiguration($oTemplate->template_name, null, null, true);
            // A possible solution to this error is to re-install the template.
            if ($templateDbConf->config->metadata->version != $oTemplate->template->version) {
                throw new WrongTemplateVersionException(
                    sprintf(
                        gT("Can't render layout %s for template %s. Template version in database is %s, but in config.xml it's %s. Please re-install the template."),
                        $sLayout,
                        $oTemplate->template_name,
                        $oTemplate->template->version,
                        $templateDbConf->config->metadata->version
                    )
                );
            }
            // TODO: Panic or default to something else?
            throw new CException(
                sprintf(
                    gT("Can't render layout %s for template %s. Please try to re-install the template."),
                    $sLayout,
                    $oTemplate->template_name
                )
            );
        }
    }

    /**
     * Main method to render a question in the question editor preview.
     * @param string $sLayout the name of the layout to render
     * @param array $aData the datas needed to fill the layout
     * @param bool $root
     * @param boolean $bReturn if true, it will return the html string without
     *                         rendering the whole page. Useful for debugging, and used for Print Answers
     * @return mixed|string
     * @throws CException
     * @throws Throwable
     * @throws Twig_Error_Loader
     * @throws Twig_Error_Syntax
     * @throws WrongTemplateVersionException
     */
    public function renderTemplateForQuestionEditPreview($sLayout, $aData, $root = false, $bReturn = false)
    {
        $root = (bool) $root;
        $oTemplate = Template::getInstance(
            $aData['aSurveyInfo']['template'],
            null,
            null,
            null,
            null,
            true
        );
        $oLayoutTemplate = $this->getTemplateForView($sLayout, $oTemplate);
        if ($oLayoutTemplate) {
            $postMessageScripts = "
var eventset = true;
window.addEventListener('message', function(event) {
    if(eventset){ console.log(event); eventset=!eventset;}
    event.source.postMessage({msg: 'EVENT COLLECTED', event: event.data}, '*');
    var getUrl = window.location;
    var baseUrl = getUrl.protocol + '//' + getUrl.host + '/' + getUrl.pathname.split('/')[1];
    
    if (baseUrl.match(event.origin) && event.data.run == 'trigger::pjax:scriptcomplete' ) {
        console.log('runScriptcomplete');
        jQuery(document).trigger('pjax:scriptcomplete');
    }

    if (baseUrl.match(event.origin) && event.data.run == 'trigger::newContent' ) {
        console.log('replaceContent');
        jQuery('#questionPreview--content').text('');
        jQuery('#questionPreview--content').html(event.data.content);
    }
}, false);  
";
            App()->getClientScript()->registerScript(
                'postMessageScripts',
                $postMessageScripts,
                CClientScript::POS_HEAD
            );
            $line = '<div class="{{ aSurveyInfo.class.outerframe }}  {% if (aSurveyInfo.options.container == "on") %} container {% else %} container-fluid {% endif %} " id="{{ aSurveyInfo.id.outerframe }}" {{ aSurveyInfo.attr.outerframe }} >';
            $line .= file_get_contents($oLayoutTemplate->viewPath . $sLayout);
            $line .= '</div>';
            if ($root === true) {
                $line = '<html lang="{{ aSurveyInfo.languagecode }}" dir="{{ aSurveyInfo.dir }}" class="{{ aSurveyInfo.languagecode }} dir-{{ aSurveyInfo.dir }} {{ aSurveyInfo.class.html }}" {{ aSurveyInfo.attr.html }}>'
                    . file_get_contents($oLayoutTemplate->viewPath . '/subviews/header/head.twig')
                    . '<body style="padding-top: 0px !important;" class=" {{ aSurveyInfo.class.body }} font-{{  aSurveyInfo.options.font }} lang-{{aSurveyInfo.languagecode}} {{aSurveyInfo.surveyformat}} {% if( aSurveyInfo.options.brandlogo == "on") %}brand-logo{%endif%}" {{ aSurveyInfo.attr.body }} >'
                    . $line;
                $line .= '</body>';
                $line .= '</html>';
            }

            $sHtml     = $this->convertTwigToHtml($line, $aData, $oTemplate);

            $sEmHiddenInputs = LimeExpressionManager::FinishProcessPublicPage(true);
            if ($sEmHiddenInputs) {
                $sHtml = str_replace(
                    "<!-- emScriptsAndHiddenInputs -->",
                    "<!-- emScriptsAndHiddenInputs updated -->\n"
                    . $sEmHiddenInputs,
                    $sHtml
                );
            }
            if ($bReturn) {
                return $sHtml;
            } else {
                $this->renderHtmlPage($sHtml, $oTemplate);
            }
        } else {
            $templateDbConf = Template::getTemplateConfiguration($oTemplate->template_name, null, null, true);
            // A possible solution to this error is to re-install the template.
            if ($templateDbConf->config->metadata->version != $oTemplate->template->version) {
                throw new WrongTemplateVersionException(
                    sprintf(
                        gT("Can't render layout %s for template %s. Template version in database is %s, but in config.xml it's %s. Please re-install the template."),
                        $sLayout,
                        $oTemplate->template_name,
                        $oTemplate->template->version,
                        $templateDbConf->config->metadata->version
                    )
                );
            }
            // TODO: Panic or default to something else?
            throw new CException(
                sprintf(
                    gT("Can't render layout %s for template %s. Please try to re-install the template."),
                    $sLayout,
                    $oTemplate->template_name
                )
            );
        }
    }


    /**
     * Main method to render an admin page or block.
     * Extendable to use admin templates in the future currently running on paths, like the yii render methods go.
     * @param $sLayoutFilePath
     * @param array $aData the datas needed to fill the layout
     * @param boolean $bReturn if true, it will return the html string without rendering the whole page.
     *                         Useful for debugging, and used for Print Answers
     * @param boolean $bUseRootDir Prepend application root dir to sLayoutFilePath if true.
     * @return string HTML
     * @throws CException
     * @throws Throwable
     * @throws Twig_Error_Loader
     * @throws Twig_Error_Syntax
     * @todo missing return statement (php warning)
     */
    public function renderViewFromFile($sLayoutFilePath, $aData, $bReturn = false, $bUseRootDir = true)
    {
        if ($bUseRootDir) {
            $viewFile = App()->getConfig('rootdir') . $sLayoutFilePath;
        } else {
            $viewFile = $sLayoutFilePath;
        }

        if (file_exists($viewFile)) {
            $line       = file_get_contents($viewFile);
            $sHtml      = $this->convertTwigToHtml($line, $aData);

            if ($bReturn) {
                return $sHtml;
            } else {
                /** @psalm-suppress UndefinedVariable TODO: $oTemplate is never defined */
                $this->renderHtmlPage($sHtml, $oTemplate);
            }
        } else {
            throw new CException(
                sprintf(
                    gT("Can't render layout %s. Please check that the view exists or contact your admin."),
                    $viewFile
                )
            );
        }
    }

    /**
     * This method is called from qanda helper to render a question view file.
     * It first checks if the question use a template (set in display attributes)
     * If it is the case, it will use the views of that template, else, it will render the core view.
     *
     * @param string $sView the view (layout) to render
     * @param array $aData the datas needed for the view rendering
     *
     * @return  string the generated html
     * @throws CException
     * @throws Twig_Error_Loader
     * @throws Twig_Error_Syntax
     */
    public function renderQuestion($sView, $aData)
    {
        $this->_twig  = parent::getTwig(); // Twig object

        // Question template instance has been created at top of qanda_helper::retrieveAnswers()
        $oQuestionTemplate   = QuestionTemplate::getInstance();
        $extraPath = array();
        // check if this method is called from theme editor
        $sTemplateFolderName = null;
        if (empty($aData['bIsThemeEditor'])) {
            // Get the name of the folder for that question type.
            $sTemplateFolderName = $oQuestionTemplate->getQuestionTemplateFolderName();
        }
        // Check if question use a custom template and that it provides its own twig view
        $sDirName = null; // Extra dir name to read from template before question template
        if ($sTemplateFolderName) {
            // A template can change only one of the view of the question type.
            // So other views should be rendered by core.
            $bTemplateHasThisView = $oQuestionTemplate->checkIfTemplateHasView($sView);
            if ($bTemplateHasThisView) {
                $sDirName = 'question' . DIRECTORY_SEPARATOR . $sTemplateFolderName;
                $extraPath[] = $oQuestionTemplate->getTemplatePath(); // Question template views path
            }
        }

        // We check if the file is a twig file or a php file
        // This allow us to twig the view one by one, from PHP to twig.
        // The check will be removed when 100% of the views will have been twig
        if ($this->getPathOfFile($sView . '.twig', null, $extraPath, $sDirName)) {
            // We're not using the Yii Theming system, so we don't use parent::renderFile
            // current controller properties will be accessible as {{ this.property }}
                        //  aData and surveyInfo variables are accessible from question type twig files
            $aData['aData'] = $aData;

            // check if this method is called from theme editor
            if (empty($aData['bIsThemeEditor'])) {
                    $aData['question_template_attribute'] = $oQuestionTemplate->getCustomAttributes();
                    $sBaseLanguage = Survey::model()->findByPk($_SESSION['LEMsid'])->language;
                    $aData['surveyInfo'] = getSurveyInfo($_SESSION['LEMsid'], $sBaseLanguage);
                    $aData['this'] = App()->getController();
            } else {
                $aData['question_template_attribute'] = null;
            }
            $template = $this->_twig->render($sView . '.twig', $aData);
            return $template;
        } else {
            return App()->getController()->renderPartial($sView, $aData, true);
        }
    }

    /**
     * This method is used to render question's subquestions and answer options pages  .
     * It first checks if the question use a template (set in display attributes)
     * If it is the case, it will use the views of that template, else, it will render the core view.
     *
     * @param string $sView the view (layout) to render
     * @param array $aData the datas needed for the view rendering
     *
     * @return  string the generated html
     * @throws CException
     * @throws Twig_Error_Loader
     * @throws Twig_Error_Syntax
     */
    public function renderAnswerOptions($sView, $aData)
    {
        $this->_twig  = parent::getTwig(); // Twig object
        $loader       = $this->_twig->getLoader(); // Twig Template loader
        // By default, the required view is the core view
        $requiredView = Yii::getPathOfAlias('application.views') . $sView;
        $loader->setPaths(App()->getBasePath() . '/views/'); // Core views path

        // Question template instance has been created at top of qanda_helper::retrieveAnswers()
        $oQuestionTemplate   = QuestionTemplate::getInstance();

        // currently, question's subquestions and answer options pages are rendered only from core
        $sTemplateFolderName = null;

        // Check if question use a custom template and that it provides its own twig view
        if ($sTemplateFolderName) {
            // A template can change only one of the view of the question type. So other
            // views should be rendered by core.
            $bTemplateHasThisView = $oQuestionTemplate->checkIfTemplateHasView($sView);

            if ($bTemplateHasThisView) {
                $sQTemplatePath = $oQuestionTemplate->getTemplatePath(); // Question template views path
                $loader->setPaths($sQTemplatePath); // Loader path
                $requiredView = $sQTemplatePath . ltrim($sView, '/'); // Complete path of the view
            }
        }

        // We check if the file is a twig file or a php file
        // This allow us to twig the view one by one, from PHP to twig.
        // The check will be removed when 100% of the views will have been twig
        if (file_exists($requiredView . '.twig')) {
            // We're not using the Yii Theming system, so we don't use parent::renderFile
            // current controller properties will be accessible as {{ this.property }}

            //  aData and surveyInfo variables are accessible from question type twig files
            $aData['aData'] = $aData;
            $sBaseLanguage = Survey::model()->findByPk($_SESSION['LEMsid'])->language;
            $aData['surveyInfo'] = getSurveyInfo($_SESSION['LEMsid'], $sBaseLanguage);
            $aData['this'] = App()->getController();

            $aData['question_template_attribute'] = null;

            $template = $this->_twig->render($sView . '.twig', $aData);
            return $template;
        } else {
            return App()->getController()->renderPartial($sView, $aData, true);
        }
    }

    /**
     * This method is called from Template Editor.
     * It returns the HTML string needed for the tmp file using the template twig file.
     * @param string $sView the view (layout) to render
     * @param array $aData the datas needed for the view rendering
     * @param Template $oEditedTemplate the template to use
     *
     * @return  string the generated html
     * @throws Throwable
     * @throws Twig_Error_Loader
     * @throws Twig_Error_Syntax
     * @todo missing return statement (php warning)
     */
    public function renderTemplateForTemplateEditor($sView, $aData, $oEditedTemplate)
    {
        $oTemplate = $this->getTemplateForView($sView, $oEditedTemplate);
        if ($oTemplate) {
            $line      = file_get_contents($oTemplate->viewPath . $sView);
            $result    = $this->convertTwigToHtml($line, $aData, $oEditedTemplate);
            return $result;
        } else {
            trigger_error("Can't find a theme for view: " . $sView, E_USER_ERROR);
        }
    }

    /**
     * Render the option page of a template for the admin interface
     * @param Template $oTemplate the template where the custom option page should be looked for
     * @param array $renderArray Array that will be passed to the options.twig as variables.
     * @return string
     * @throws Throwable
     * @throws Twig_Error_Loader
     * @throws Twig_Error_Syntax
     */
    public function renderOptionPage($oTemplate, $renderArray = array())
    {
        $oRTemplate = $oTemplate;
        $sOptionFile = 'options/options.twig';
        $sOptionJS   = 'options/options.js';
        $sOptionsPath = $oRTemplate->sTemplateurl . 'options';

        // We get the options twig file from the right template (local or mother template)
        while (!file_exists($oRTemplate->path . $sOptionFile)) {
            $oMotherTemplate = $oRTemplate->oMotherTemplate;
            if (!($oMotherTemplate instanceof TemplateConfiguration)) {
                return sprintf(gT('%s not found!', $oRTemplate->path . $sOptionFile));
                break;
            }
            $oRTemplate = $oMotherTemplate;
            $sOptionsPath = $oRTemplate->sTemplateurl . 'options';
        }

        if (file_exists($oRTemplate->path . $sOptionJS)) {
            App()->getClientScript()->registerScriptFile(
                $oRTemplate->sTemplateurl . $sOptionJS,
                LSYii_ClientScript::POS_END
            );
        }

        $this->_twig = $twig = parent::getTwig();
        $this->addRecursiveTemplatesPath($oRTemplate);
        $renderArray['optionsPath'] = $sOptionsPath;
        $renderArray['showpopups_disabled'] = (int) App()->getConfig('showpopups') < 2 ? 'disabled' : '';
        $renderArray['showpopups_disabled_qtip'] = (int) App()->getConfig('showpopups') < 2
            ? 'data-hasqtip="true" title="' .
            gT("Disabled by configuration. Set 'showpopups' option in config.php file to enable this option. ") .
            '"'
            : '';
        // Twig rendering
        $line         = file_get_contents($oRTemplate->path . $sOptionFile);
        $oTwigTemplate = $twig->createTemplate($line);
        $sHtml        = $oTwigTemplate->render($renderArray, false);

        return $sHtml;
    }

    /**
     * Render the survey page, with the headers, the css, and the script
     * If LS would use the normal Yii render flow, this function would not be necessary
     * In previous LS version, this logic was here: https://github.com/LimeSurvey/LimeSurvey/blob/700b20e2ae918550bfbf283f433f07622480978b/application/controllers/survey/index.php#L62-L71
     *
     * @param string $sHtml     The Html content of the page (it must not contain anymore any twig statement)
     * @param Template $oTemplate The name of the template to use to register the packages
     */
    public function renderHtmlPage($sHtml, $oTemplate)
    {
        App()->clientScript->registerPackage($oTemplate->sPackageName, LSYii_ClientScript::POS_BEGIN);

        ob_start(function ($buffer, $phase) {
            App()->getClientScript()->render($buffer);
            App()->getClientScript()->reset();
            return $buffer;
        });

        ob_implicit_flush(false);
        echo $sHtml;
        ob_flush();

        App()->end();
    }


    /**
     * Convert a string containing twig tags to an HTML string.
     *
     * @param string $sString The string of HTML/Twig to convert
     * @param array $aData Array containing the datas needed to render the view ($thissurvey)
     * @param TemplateConfiguration|null $oTemplate
     * @return string
     * @throws Throwable
     * @throws Twig_Error_Loader
     * @throws Twig_Error_Syntax
     */
    public function convertTwigToHtml($sString, $aData = array(), $oTemplate = null)
    {

        // Twig init
        $this->_twig = $twig = parent::getTwig();

        //Run theme related things only if a theme is provided!
        if ($oTemplate !== null) {
            // Get the additional infos for the view, such as language, direction, etc
            $aData = $this->getAdditionalInfos($aData, $oTemplate);

            // Add to the loader the path of the template and its parents.
            $this->addRecursiveTemplatesPath($oTemplate);

            // Plugin for blocks replacement
            list($sString, $aData) = $this->getPluginsData($sString, $aData);
        }

        // Twig rendering
        $oTwigTemplate = $twig->createTemplate($sString);
        $sHtml         = $oTwigTemplate->render($aData, false);

        return $sHtml;
    }

    /**
     * Find which template should be used to render a given view
     * @param  string    $sView           the view (layout) to render
     * @param  TemplateConfiguration  $oRTemplate    the template where the custom option page should be looked for
     * @return Template|boolean
     */
    private function getTemplateForView($sView, $oRTemplate)
    {
        while (!file_exists($oRTemplate->viewPath . $sView)) {
            $oMotherTemplate = $oRTemplate->oMotherTemplate;
            if (!($oMotherTemplate instanceof TemplateConfiguration)) {
                return false;
                break;
            }
            $oRTemplate = $oMotherTemplate;
        }

        return $oRTemplate;
    }

    /**
     * Twig can look for twig path in different path. This function will add the path of the template and all its
     * parents to the load path
     * So if a twig file is inclueded, it will look in the local template directory and all its parents
     * @param TemplateConfiguration $oTemplate  the template where to start
     * @param string[] $extraPaths to be added before template, parent template plugin add and core views.
     * Example : question template
     * @param string|null $dirName directory name to be added as extra directory inside template view
     */
    private function addRecursiveTemplatesPath($oTemplate, $extraPaths = array(), $dirName = null)
    {
        $oRTemplate   = $oTemplate;
        $loader       = $this->_twig->getLoader();
        /* Always reset (needed for Question template / $extraPaths, maybe in some other situation) */
        $loader->setPaths(array());
        /* Event to add or replace twig views */

        if (! App()->getConfig('force_xmlsettings_for_survey_rendering')) {
            $oEvent = new PluginEvent('getPluginTwigPath');
            App()->getPluginManager()->dispatchEvent($oEvent);
            $configTwigExtendsAdd = (array) $oEvent->get("add");
            $configTwigExtendsReplace = (array) $oEvent->get("replace");

            /* Forced twig by plugins (used to replace vanilla or core template …
            don't like to force on user template, but else can extend current core twig) */
            foreach ($configTwigExtendsReplace as $configTwigExtendReplace) {
                if (is_string($configTwigExtendReplace)) { // Need more control ?
                    $loader->addPath($configTwigExtendReplace);
                }
            }
        }

        if (!empty($dirName)) {
            /* This template for dirName template*/
            if (is_dir($oRTemplate->viewPath . $dirName)) {
                $loader->addPath($oRTemplate->viewPath . $dirName . DIRECTORY_SEPARATOR);
            }
            /* Parent template (for question)*/
            while ($oRTemplate->oMotherTemplate instanceof TemplateConfiguration) {
                $oRTemplate = $oRTemplate->oMotherTemplate;
                if (is_dir($oRTemplate->viewPath . $dirName)) {
                    $loader->addPath($oRTemplate->viewPath . $dirName . DIRECTORY_SEPARATOR);
                }
            }
        }
        /* Extra path (Question template Path for example)*/
        if (!empty($extraPaths)) {
            foreach ($extraPaths as $extraPath) {
                $loader->addPath($extraPath);
            }
        }
        $oRTemplate   = $oTemplate;
        /* This template */
        $loader->addPath($oRTemplate->viewPath);
        /* Parent template */
        while ($oRTemplate->oMotherTemplate instanceof TemplateConfiguration) {
            $oRTemplate = $oRTemplate->oMotherTemplate;
            $loader->addPath($oRTemplate->viewPath);
        }
        /* Added twig by plugins, replaced by any template file or question template file*/
        if (isset($configTwigExtendsAdd)) {
            foreach ($configTwigExtendsAdd as $configTwigExtendAdd) {
                if (is_string($configTwigExtendAdd)) {
                    $loader->addPath($configTwigExtendAdd);
                }
            }
        }

        $loader->addPath(App()->getBasePath() . '/views/'); // Core views path
    }

    /**
     * Plugin event, should be replaced by Question Template
     * @param string $sString
     * @param array $aData
     * @return array
     */
    private function getPluginsData($sString, $aData)
    {
        $event = new PluginEvent('beforeTwigRenderTemplate');

        if (!empty($aData['aSurveyInfo']['sid'])) {
            $surveyid = $aData['aSurveyInfo']['sid'];
            $event->set('surveyId', $aData['aSurveyInfo']['sid']);

            // show "Exit and clear survey" button whenever there is 'srid' key set,
            // button won't be rendered on welcome and final page because 'srid' key doesn't exist on those pages
            // additionally checks for submit page to compensate when srid is needed to render other views
            if (
                isset($_SESSION['responses_' . $surveyid]['srid'])
                && isset($aData['aSurveyInfo']['active']) && $aData['aSurveyInfo']['active'] == 'Y'
                && isset($aData['aSurveyInfo']['include_content']) && $aData['aSurveyInfo']['include_content'] !== 'submit'
                && isset($aData['aSurveyInfo']['include_content']) && $aData['aSurveyInfo']['include_content'] !== 'submit_preview'
            ) {
                $aData['aSurveyInfo']['bShowClearAll'] = true;
            }
        }

        if (!App()->getConfig('force_xmlsettings_for_survey_rendering')) {
            App()->getPluginManager()->dispatchEvent($event);
            $aPluginContent = $event->getAllContent();
            if (!empty($aPluginContent['sTwigBlocks'])) {
                $sString = $sString . $aPluginContent['sTwigBlocks'];
            }
        }

        return array($sString, $aData);
    }

    /**
     * In the LS2.x code, some of the render logic was duplicated on different files
     * (surveyRuntime, frontend_helper, etc)
     * In LS3, we did a first cycle of refactorisation. Some logic common to the different
     * files are for now here, in this function.
     *
     * @todo move all the display logic to surveyRuntime so we don't need this function here
     * @param TemplateConfiguration $oTemplate
     * @return array
     */
    private function getAdditionalInfos($aData, $oTemplate)
    {
        /* get minimal surveyInfo if we can have a sid, used in ExpressionManager for example */
        if (empty($aData["aSurveyInfo"])) {
            $aData["aSurveyInfo"] = array();
            if (!empty($aData["sid"]) || LimeExpressionManager::getLEMsurveyId()) {
                $sid = empty($aData["sid"]) ? LimeExpressionManager::getLEMsurveyId() : $aData["sid"];
                $language = empty($aData["language"]) ? App()->getLanguage() : $aData["language"];
                /* Outdated sid in LimeExpressionManager */
                if (Survey::model()->findByPk($sid)) {
                    $aData["aSurveyInfo"] = getSurveyInfo($sid, $language);
                }
            }
        }
        // We retrieve the definition of the core class and attributes
        // (in the future, should be template dependant done via XML file)
        $aData["aSurveyInfo"] = array_merge($aData["aSurveyInfo"], $oTemplate->getClassAndAttributes());

        $languagecode = App()->getLanguage();
        if (!empty($aData['aSurveyInfo']['sid']) && Survey::model()->findByPk($aData['aSurveyInfo']['sid'])) {
            if (!in_array($languagecode, Survey::model()->findByPk($aData['aSurveyInfo']['sid'])->getAllLanguages())) {
                $languagecode = Survey::model()->findByPk($aData['aSurveyInfo']['sid'])->language;
            }
        }

        $aData["aSurveyInfo"]['languagecode']     = $languagecode;
        $aData["aSurveyInfo"]['dir']              = (getLanguageRTL($languagecode)) ? "rtl" : "ltr";

        if (!empty($aData['aSurveyInfo']['sid'])) {
            $showxquestions                            = App()->getConfig('showxquestions');
            $aData["aSurveyInfo"]['bShowxquestions']  = ($showxquestions == 'show' ||
                ($showxquestions == 'choose' && !isset($aData['aSurveyInfo']['showxquestions'])) ||
                ($showxquestions == 'choose' && $aData['aSurveyInfo']['showxquestions'] == 'Y'));


            // NB: Session is flushed at submit, so sid is not defined here.
            if (
                isset($_SESSION['responses_' . $aData['aSurveyInfo']['sid']]) &&
                isset($_SESSION['responses_' . $aData['aSurveyInfo']['sid']]['totalquestions'])
            ) {
                $aData["aSurveyInfo"]['iTotalquestions'] = $_SESSION['responses_' .
                $aData['aSurveyInfo']['sid']]['totalVisibleQuestions'];
            }

            // Add the survey theme options
            if ($oTemplate->oOptions) {
                foreach ($oTemplate->oOptions as $key => $value) {
                    // TODO: Same issue as commit 2972aea41c51c74db95bfe40c337ae839471152c
                    // Options are not loaded the same way in all places.
                    if ($value instanceof stdClass) {
                        $value = 'N/A';
                    }
                    // Note that $value can also be a SimpleXMLElement
                    // if force_xmlsettings_for_survey_rendering is activated
                    $aData["aSurveyInfo"]["options"][$key] = (string)$value;
                }
            }
            $aData["aSurveyInfo"] = $this->setDefaultPrivacyText($aData["aSurveyInfo"]);
        } else {
            // Add the global theme options
            $oTemplateConfigurationCurrent = Template::getInstance($oTemplate->sTemplateName);
            $aData["aSurveyInfo"]["options"] = isJson($oTemplateConfigurationCurrent['options'])
                ? json_decode((string) $oTemplateConfigurationCurrent['options'], true)
                : $oTemplateConfigurationCurrent['options'];
        }

        $aData = $this->fixDataCoherence($aData);

        return $aData;
    }

    /**
         * Ensure privacy text strings exist and render the privacy notice label.
         *
         * If specific privacy strings are empty, sets sensible defaults and renders
         * the `datasecurity_notice_label` using the privacy twig partial. This
         * operation runs only once per request; subsequent calls return the input unchanged.
         *
         * @param array $aSurveyInfo Survey rendering data; must contain at least `sid` when available.
         * @return array The updated `$aSurveyInfo` array with `datasecurity_notice_label` and `datasecurity_error` ensured and the notice label rendered as HTML.
         */
    private function setDefaultPrivacyText($aSurveyInfo)
    {
        /* Do it one time only (and do not recall self when using renderPartial) */
        static $DefaultPrivacyDone = false;
        if ($DefaultPrivacyDone) {
            return $aSurveyInfo;
        }
        $DefaultPrivacyDone = true;
        if (empty($aSurveyInfo['datasecurity_notice_label'])) {
            $aSurveyInfo['datasecurity_notice_label'] = gT("To continue please first accept our survey privacy policy.");
        }
        if (empty($aSurveyInfo['datasecurity_error'])) {
            $aSurveyInfo['datasecurity_error'] = gT("We are sorry but you can't proceed without first agreeing to our survey privacy policy.");
        }
        /* @var string[] for automatic translation */
        $translation = [
            "Show policy" => gT("Show policy")
        ];
        $aSurveyInfo['datasecurity_notice_label'] =  $this->renderPartial(
            './subviews/privacy/privacy_datasecurity_notice_label.twig',
            [
                'dataSecurityNoticeLabel' => $aSurveyInfo['datasecurity_notice_label'],
                'sid' => $aSurveyInfo['sid'],
            ]
        );
        return $aSurveyInfo;
    }

    /**
         * Ensure option flags that depend on files are coherent.
         *
         * If a file-related option (e.g., `brandlogofile`, `backgroundimagefile`) is empty,
         * the corresponding boolean-like option (`brandlogo`, `backgroundimage`) is set to `"false"`.
         *
         * @param array $aData Rendering data (expects `aSurveyInfo['options']` when present).
         * @return array The input `$aData` with corrected option flags where applicable.
         */
    private function fixDataCoherence($aData)
    {
        // Clean option with files
        $aFilesOptions = array( 'brandlogo' => 'brandlogofile'  , 'backgroundimage' => 'backgroundimagefile' );

        foreach ($aFilesOptions as $sOption => $sFileOption) {
            if (isset($aData["aSurveyInfo"]["options"]) && is_array($aData["aSurveyInfo"]["options"])) {
                if (array_key_exists($sFileOption, $aData["aSurveyInfo"]["options"])) {
                    if (empty($aData["aSurveyInfo"]["options"][$sFileOption])) {
                        $aData["aSurveyInfo"]["options"][$sOption] = "false";
                    }
                }
            }
        }

        return $aData;
    }


    /**
     * Adds custom extensions.
     * It's different from the original Yii Twig Extension to take in account our custom sand box
     * @param array $extensions @see self::$extensions
     */
    public function addExtensions($extensions)
    {
        $this->_twig = parent::getTwig();
        foreach ($extensions as $extName) {
            if ($extName == "\Twig\Extension\SandboxExtension") {
                // Process to load the sandBox
                $tags       = $this->sandboxConfig['tags'] ?? array();
                $filters    = $this->sandboxConfig['filters'] ?? array();
                $methods    = $this->sandboxConfig['methods'] ?? array();
                $properties = $this->sandboxConfig['properties'] ?? array();
                $functions  = $this->sandboxConfig['functions'] ?? array();
                $policy     = new \Twig\Sandbox\SecurityPolicy($tags, $filters, $methods, $properties, $functions);
                $sandbox    = new \Twig\Extension\SandboxExtension($policy, true);

                $this->_twig->addExtension($sandbox);
            } else {
                $this->_twig->addExtension(new $extName());
            }
        }
    }

    /**
     * get a twig file and return html produced
     * @todo find a way to fix in beforeCloseHtml @see https://bugs.limesurvey.org/view.php?id=13889
     * @param string $twigView twigfile to be used (with twig extension)
     * @param array $aData to be used
     * @return string
     * @throws Twig_Error_Loader
     * @throws Twig_Error_Syntax
     */
    public function renderPartial($twigView, $aData)
    {
        $oTemplate = Template::getLastInstance();
        $aData = $this->getAdditionalInfos($aData, $oTemplate);
        $this->addRecursiveTemplatesPath($oTemplate);
        return $this->_twig->render($twigView, $aData);
    }

    /**
     * Get the final source file for current context
     * Currently used in theme editor
     * @param string $twigView twigfile to be used (with twig extension)
     * @param TemplateConfiguration $oTemplate
     * @param string[] $extraPath path to be added before plugins add and core views
     * @param string|null $dirName directory name to be added as extra directory inside template view
     * @return string complete filename to be used
     * @throws Exception
     */
    public function getPathOfFile($twigView, $oTemplate = null, $extraPath = array(), $dirName = null)
    {
        if (!$oTemplate) {
            $oTemplate = Template::getLastInstance();
        }
        $this->addRecursiveTemplatesPath($oTemplate, $extraPath, $dirName);
        if (!$this->_twig->getLoader()->exists($twigView)) {
            return null;
        }
        return $this->_twig->getLoader()->getSourceContext($twigView)->getPath();
    }

    /**
     * This is used to add paths in controller views.
     *
     * @return FileLoader
     */
    public function getLoader()
    {
        return $this->_twig->getLoader();
    }

    /**
     * Adds custom user extensions
     * @param array $extensions @see self::$user_extensions
     */
    public function addUserExtensions($extensions)
    {
        foreach ($extensions as $extName) {
            Yii::setPathOfAlias('extName_' . $extName, Yii::app()->getConfig('usertwigextensionrootdir') . '/' . $extName . '/');
            Yii::import("extName_" . $extName . ".*");
            $this->_twig->addExtension(new $extName());
        }
    }
}
