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

/*
* LimeSurvey
* Copyright (C) 2007-2015 The LimeSurvey Project Team / Carsten Schmitz
* All rights reserved.
* License: GNU/GPL License v2 or later, see LICENSE.php
* LimeSurvey is free software. This version may have been modified pursuant
* to the GNU General Public License, and as distributed it includes or
* is derivative of works licensed under the GNU General Public License or
* other free or open source software licenses.
* See COPYRIGHT.php for copyright notices and details.
*/

/**
 * This is the model class for table "{{template_configuration}}".
 *
 * NOTE: if you only need to access to the table, you don't need to call prepareTemplateRendering
 *
 * The following are the available columns in table '{{template_configuration}}':
 * @property integer $id Primary key
 * @property string $template_name
 * @property integer $sid Survey ID
 * @property integer $gsid
 * @property integer $uid user ID
 * @property string $files_css
 * @property string $files_js
 * @property string $files_print_css
 * @property string $options
 * @property string $cssframework_name
 * @property string $cssframework_css
 * @property string $cssframework_js
 * @property string $packages_to_load
 * @property string $packages_ltr
 * @property string $packages_rtl
 * @property Template $template
 *
 * @package       LimeSurvey
 * @subpackage    Backend
 */
class TemplateConfiguration extends TemplateConfig
{
    /**
     * @var TemplateConfiguration $oParentTemplate The parent template name
     * A template configuration, in the database, can inherit from another one.
     * This used to manage the different configuration levels for a very same template: global, survey group, survey
     * This is not related to motherTemplate (inheritance between two different templates)
     */
    public $oParentTemplate;

    /**@var boolean
     * Should the magic getters automatically retreives the parent value when field is set to inherit. Only turn to
     * on for template rendering. There is no option inheritance on Manifest mode: values from XML are always used.
     */
    public $bUseMagicInherit = false;

    /**@var boolean
     * Indicate if this entry in DB get created on the fly. If yes, because of Cache, it can need a page redirect
     */
    public $bJustCreated = false;

    // Caches

    /** @var string $sPreviewImgTag the template preview image tag for the template list*/
    public $sPreviewImgTag;

    /** @var array $aInstancesFromTemplateName cache for method getInstanceFromTemplateName*/
    public static $aInstancesFromTemplateName;

    /** @var array $aPreparedToRender cache for method prepareTemplateRendering*/
    public static $aPreparedToRender;

    /** @var boolean $bTemplateCheckResult is the template valid?*/
    private $bTemplateCheckResult;

    /** @var string $sTypeIcon the type of template for icon (core vs user)*/
    private $sTypeIcon;

    /** @var array $aFilesTo cache for the method getFilesTo*/
    private $aFilesTo;

    /** @var array $aFrameworkAssetsToReplace cache for the method getFrameworkAssetsToReplace()*/
    private $aFrameworkAssetsToReplace;

    /** @var array $aReplacements cache for the method getFrameworkAssetsReplacement */
    private $aReplacements;

    /** @var array $Ofiles cache for the method getOfiles */
    private $Ofiles;

    public $generalFilesPath; //Yii::app()->getConfig("userthemerootdir").DIRECTORY_SEPARATOR.'generalfiles'.DIRECTORY_SEPARATOR;

    /** @var int $showpopups show warnings when running survey */
    public $showpopups; //

    // for survey theme gridview columns
    public $template_type;
    public $template_extends;
    public $template_description;


    /**
     * @todo document me
     * @return string the associated database table name
     */
    public function tableName()
    {
        return '{{template_configuration}}';
    }

    /**
     * @todo document me
     * @return array validation rules for model attributes.
     */
    public function rules()
    {
        // NOTE: you should only define rules for those attributes that
        // will receive user inputs.
        return array(
            array('template_name', 'required'),
            array('id, sid, gsid', 'numerical', 'integerOnly' => true),
            array('template_name', 'filter', 'filter' => function ($value) {
                return sanitize_filename($value, false, false, false);
            }),
            array('template_name', 'length', 'max' => 150),
            array('cssframework_name', 'length', 'max' => 45),
            array('files_css, files_js, files_print_css, options, cssframework_css, cssframework_js, packages_to_load',
                'safe'),
            array('options', 'sanitizeImagePathsOnJson'),
            // The following rule is used by search().
            array('id, template_name, sid, gsid, files_css, files_js, files_print_css, options, cssframework_name, cssframework_css, cssframework_js, packages_to_load', 'safe', 'on' => 'search'),
        );
    }

    /**
     * @todo document me
     * @return array relational rules.
     */
    public function relations()
    {
        return array(
            'template' => array(self::HAS_ONE, 'Template', array('name' => 'template_name'), 'together' => true),
        );
    }

    /**
     * @todo document me
     *
     * @return array customized attribute labels (name=>label)
     */
    public function attributeLabels()
    {
        return array(
            'id' => 'ID',
            'template_name' => gT('Template name'),
            'sid' => 'Sid',
            'gsid' => 'Gsid',
            'files_css' => gT('Files CSS'),
            'files_js' => gT('Files JS'),
            'files_print_css' => gT('Files Print CSS'),
            'options' => gT('Options'),
            'cssframework_name' => gT('CSS framework name'),
            'cssframework_css' => gT('CSS framework CSS'),
            'cssframework_js' => gT('CSS framework JS'),
            'packages_to_load' => gT('Packages to load'),
        );
    }

    /**
     * Gets an instance of a templateconfiguration by name
     *
     * @param string $sTemplateName
     * @param boolean $abstractInstance
     * @return TemplateConfiguration
     */
    public static function getInstanceFromTemplateName($sTemplateName, $abstractInstance = false)
    {
        if (!empty(self::$aInstancesFromTemplateName[$sTemplateName]) && !$abstractInstance) {
            return self::$aInstancesFromTemplateName[$sTemplateName];
        }

        $oInstance = self::model()->find(
            'template_name=:template_name AND sid IS NULL AND gsid IS NULL',
            array(':template_name' => $sTemplateName)
        );

        // If the survey configuration table of the wanted template doesn't exist (eg: manually deleted),
        // then we provide the default one.
        if (!is_a($oInstance, 'TemplateConfiguration')) {
            $oInstance = self::getInstanceFromTemplateName(App()->getConfig('defaulttheme'));
        }

        if ($abstractInstance === true) {
            return $oInstance;
        }

        self::$aInstancesFromTemplateName[$sTemplateName] = $oInstance;

        return $oInstance;
    }

    /**
     * Returns a TemplateConfiguration Object based on a surveygroup ID
     * If no instance is existing, it will create one.
     *
     * @param integer $iSurveyGroupId
     * @param string $sTemplateName
     * @param boolean $abstractInstance
     * @return TemplateConfiguration
     */
    public static function getInstanceFromSurveyGroup($iSurveyGroupId, $sTemplateName = null, $abstractInstance = false)
    {
        //if a template name is given also check against that
        $sTemplateName = $sTemplateName != null
            ? $sTemplateName
            : SurveysGroups::model()->findByPk($iSurveyGroupId)->template;

        $criteria = new CDbCriteria();
        $criteria->addCondition('gsid=:gsid');
        $criteria->addCondition('template_name=:template_name');
        $criteria->params = array('gsid' => $iSurveyGroupId, 'template_name' => $sTemplateName);
        $oTemplateConfigurationModel = TemplateConfiguration::model()->find($criteria);

        // No specific template configuration for this surveygroup => create one
        // TODO: Move to SurveyGroup creation, right now the 'lazy loading' approach is ok.
        if (!is_a($oTemplateConfigurationModel, 'TemplateConfiguration') && $sTemplateName != null) {
            $oTemplateConfigurationModel = TemplateConfiguration::getInstanceFromTemplateName(
                $sTemplateName,
                $abstractInstance
            );
            $oTemplateConfigurationModel->bUseMagicInherit = false;
            $oTemplateConfigurationModel->id = null;
            $oTemplateConfigurationModel->isNewRecord = true;
            $oTemplateConfigurationModel->sid = null;
            $oTemplateConfigurationModel->gsid = $iSurveyGroupId;
            $oTemplateConfigurationModel->setToInherit();
            $oTemplateConfigurationModel->save();

            $oTemplateConfigurationModel->bJustCreated = true;
        }

        return $oTemplateConfigurationModel;
    }

    /**
     * Returns a TemplateConfiguration Object based on a surveyID
     * If no instance is existing, it will create one.
     *
     * @param integer $iSurveyId
     * @param string $sTemplateName
     * @param boolean $abstractInstance
     * @return TemplateConfiguration
     */
    public static function getInstanceFromSurveyId($iSurveyId, $sTemplateName = null, $abstractInstance = false)
    {
        // set template name if it does not exists or if it is inherit
        if ($sTemplateName === null || $sTemplateName == 'inherit') {
            $oSurvey = Survey::model()->findByPk($iSurveyId);
            // set real value from inheritance
            if (!empty($oSurvey->oOptions->template)) {
                $sTemplateName = $oSurvey->oOptions->template;
            }
        }

        $criteria = new CDbCriteria();
        $criteria->addCondition('sid=:sid');
        $criteria->addCondition('template_name=:template_name');
        $criteria->params = array('sid' => $iSurveyId, 'template_name' => $sTemplateName);

        $oTemplateConfigurationModel = TemplateConfiguration::model()->find($criteria);

        // If the TemplateConfiguration could not be found go up the inheritance hierarchy
        if (empty($oTemplateConfigurationModel)) {
            $oTemplateConfigurationModel = TemplateConfiguration::getInstanceFromTemplateName(
                $sTemplateName,
                $abstractInstance
            );
            $oTemplateConfigurationModel->bUseMagicInherit = false;
            $oTemplateConfigurationModel->id = null;
            $oTemplateConfigurationModel->isNewRecord = true;
            $oTemplateConfigurationModel->gsid = null;
            $oTemplateConfigurationModel->sid = $iSurveyId;
            $oTemplateConfigurationModel->setToInherit();
            $oTemplateConfigurationModel->save();
        }

        return $oTemplateConfigurationModel;
    }

    /**
     * Returns a Theme options array based on a surveyID
     *
     * @param integer $iSurveyId
     * @param bool $bInherited should inherited theme option values be used?
     * @return array
     */
    public static function getThemeOptionsFromSurveyId($iSurveyId = 0, $bInherited = false)
    {
        $aTemplateConfigurations = array();
        // fetch all themes belonging to $iSurveyId
        $criteria = new CDbCriteria();
        $criteria->addCondition('sid=:sid');
        $criteria->params = array('sid' => $iSurveyId);
        $oTemplateConfigurations = self::model()->findAll($criteria);

        if ($bInherited) { // inherited values
            foreach ($oTemplateConfigurations as $key => $oTemplateConfiguration) {
                $oTemplateConfiguration->bUseMagicInherit = true;
                $oTemplateConfiguration->setOptions();
                // set array with values
                $aTemplateConfigurations[$key]['id'] = null;
                $aTemplateConfigurations[$key]['sid'] = $iSurveyId;
                $aTemplateConfigurations[$key]['template_name'] = $oTemplateConfiguration->template_name;
                $aTemplateConfigurations[$key]['config']['options'] = (array)$oTemplateConfiguration->oOptions;
            }
        } else { // db values
            foreach ($oTemplateConfigurations as $key => $oTemplateConfiguration) {
                $oTemplateConfiguration->bUseMagicInherit = false;
                $oAttributes = $oTemplateConfiguration->attributes;
                // set array with values
                $aTemplateConfigurations[$key]['id'] = null;
                $aTemplateConfigurations[$key]['sid'] = $iSurveyId;
                $aTemplateConfigurations[$key]['template_name'] = $oAttributes['template_name'];
                $aTemplateConfigurations[$key]['config']['options'] = isJson($oAttributes['options'])
                    ? json_decode((string) $oAttributes['options'], true)
                    : $oAttributes['options'];
            }
        }

        return $aTemplateConfigurations;
    }

    /**
     * For a given survey, it checks if its theme have a all the needed configuration entries (survey + survey group).
     * Else, it will create it.
     * @param int $iSurveyId
     * @return TemplateConfiguration the template configuration for the survey group
     */
    public static function checkAndcreateSurveyConfig($iSurveyId)
    {
        $oSurvey = Survey::model()->findByPk($iSurveyId);
        $iSurveyGroupId = $oSurvey->gsid;
        // set real value from inheritance
        $sTemplateName = $oSurvey->oOptions->template;

        // load or create a new entry if none is found based on $iSurveyId
        self::getInstanceFromSurveyId($iSurveyId, $sTemplateName);

        // load or create a new entry if none is found based on $iSurveyGroupId
        $oGroupTemplateConfigurationModel = self::getInstanceFromSurveyGroup($iSurveyGroupId, $sTemplateName);

        return $oGroupTemplateConfigurationModel;
    }

    /**
     * Get an instance of a fitting TemplateConfiguration
     * NOTE: for rendering prupose, you should never call this function directly, but rather Template::getInstance.
     * if force_xmlsettings_for_survey_rendering is on, then the configuration from the XML file should be loaded,
     * not the one from database
     *
     * @param string $sTemplateName
     * @param integer $iSurveyGroupId
     * @param integer $iSurveyId
     * @return TemplateConfiguration
     */
    public static function getInstance($sTemplateName = null, $iSurveyGroupId = null, $iSurveyId = null, $abstractInstance = false)
    {
        $oTemplateConfigurationModel = new TemplateConfiguration();

        if ($sTemplateName != null && $iSurveyGroupId == null && $iSurveyId == null) {
            $oTemplateConfigurationModel = TemplateConfiguration::getInstanceFromTemplateName(
                $sTemplateName,
                $abstractInstance
            );
        }

        if ($iSurveyGroupId != null && $iSurveyId == null) {
            $oTemplateConfigurationModel = TemplateConfiguration::getInstanceFromSurveyGroup(
                $iSurveyGroupId,
                $sTemplateName,
                true
            );
        }

        if ($iSurveyId != null) {
            $oTemplateConfigurationModel = TemplateConfiguration::getInstanceFromSurveyId(
                $iSurveyId,
                $sTemplateName,
                true
            );
        }

        return $oTemplateConfigurationModel;
    }

    /**
     * Retrieves a list of models based on the current search/filter conditions.
     *
     * Typical usecase:
     * - Initialize the model fields with values from filter form.
     * - Execute this method to get CActiveDataProvider instance which will filter
     * models according to data in model fields.
     * - Pass data provider to CGridView, CListView or any similar widget.
     *
     * @return CActiveDataProvider the data provider that can return the models
     * based on the search/filter conditions.
     */
    public function search()
    {
        // @todo Please modify the following code to remove attributes that should not be searched.

        $criteria = new LSDbCriteria();

        $criteria->join = 'INNER JOIN {{templates}} AS tmpl ON ' .
            App()->db->quoteColumnName("t.template_name") .
            ' = tmpl.name';
        //Don't show survey specific settings on the overview
        $criteria->addCondition('t.sid IS NULL');
        $criteria->addCondition('t.gsid IS NULL');
        $criteria->addInCondition('tmpl.name', array_keys(Template::getTemplateList()));

        $criteria->compare('id', $this->id);
        $criteria->compare('template_name', $this->template_name, true);
        $criteria->compare('files_css', $this->files_css, true);
        $criteria->compare('files_js', $this->files_js, true);
        $criteria->compare('files_print_css', $this->files_print_css, true);
        $criteria->compare('options', $this->options, true);
        $criteria->compare('cssframework_name', $this->cssframework_name, true);
        $criteria->compare('cssframework_css', $this->cssframework_css, true);
        $criteria->compare('cssframework_js', $this->cssframework_js, true);
        $criteria->compare('packages_to_load', $this->packages_to_load, true);

        return new CActiveDataProvider($this, array(
            'criteria' => $criteria
        ));
    }

    /**
     * Retrieves a list of models based on the current search/filter conditions.
     *
     * @param integer $gsid Survey group, else get global
     * @return CActiveDataProvider
     * @throws Exception
     */
    public function searchGrid(?int $gsid = null)
    {
        // @todo Please modify the following code to remove attributes that should not be searched.

        $pageSizeTemplateView = App()->user->getState('pageSizeTemplateView', App()->params['defaultPageSize']);
        $criteria = new LSDbCriteria();

        $criteria->join = 'INNER JOIN {{templates}} AS template ON ' .
            App()->db->quoteColumnName("t.template_name") .
            ' = template.name';
        $criteria->together = true;
        //Don't show surveyspecifi settings on the overview
        $criteria->addCondition('t.sid IS NULL');
        $criteria->addInCondition('template.name', array_keys(Template::getTemplateList()));

        if ($gsid !== null) {
            /* Group configuration */
            $criteria->compare('t.gsid', $gsid);
        } else {
            /* Global configuration */
            $criteria->addCondition('t.gsid IS NULL');
        }

        $criteria->compare('id', $this->id);
        if (!empty($this->template_name)) {
            $templateNameEscaped = strtr($this->template_name, ['%' => '\%', '_' => '\_', '\\' => '\\\\']);
            $criteria->addCondition('template.name LIKE :templatename1 OR template.title LIKE :templatename2');
            $criteria->params[':templatename1'] = '%' . $templateNameEscaped . '%';
            $criteria->params[':templatename2'] = '%' . $templateNameEscaped . '%';
        }
        $criteria->compare('files_css', $this->files_css, true);
        $criteria->compare('files_js', $this->files_js, true);
        $criteria->compare('files_print_css', $this->files_print_css, true);
        $criteria->compare('options', $this->options, true);
        $criteria->compare('cssframework_name', $this->cssframework_name, true);
        $criteria->compare('cssframework_css', $this->cssframework_css, true);
        $criteria->compare('cssframework_js', $this->cssframework_js, true);
        $criteria->compare('packages_to_load', $this->packages_to_load, true);
        $criteria->compare('template.description', $this->template_description, true);
        $criteria->compare('template.extends', $this->template_extends, true);

        Yii::import('application.helpers.SurveyThemeHelper');
        $coreTemplates = SurveyThemeHelper::getStandardTemplateList();
        if ($this->template_type == 'core') {
            $criteria->addInCondition('template_name', $coreTemplates);
        } elseif ($this->template_type == 'user') {
            $criteria->addNotInCondition('template_name', $coreTemplates);
        }

        return new CActiveDataProvider($this, array(
            'criteria' => $criteria,
            'pagination' => array(
                'pageSize' => $pageSizeTemplateView,
            ),
            'sort' => array(
                'defaultOrder' => 'template_name ASC' // Set default order here
            ),
        ));
    }

    /**
     * Twig statements can be used in Theme description
     */
    public function getDescription()
    {
        $sDescription = $this->template->description;

          // If wrong Twig in manifest, we don't want to block the whole list rendering
          // Note: if no twig statement in the description, twig will just render it as usual
        try {
            $sDescription = App()->twigRenderer->convertTwigToHtml($this->template->description);
            $sDescription = viewHelper::purified($sDescription);
        } catch (\Exception $e) {
          // It should never happen, but let's avoid to anoy final user in production mode :)
            if (YII_DEBUG) {
                App()->setFlashMessage(
                    "Twig error in template " .
                    $this->template->name .
                    " description <br> Please fix it and reset the theme <br>" .
                    $e,
                    'error'
                );
            }
        }

          return $sDescription;
    }

    /**
     * Returns the static model of the specified AR class.
     * Please note that you should have this exact method in all your CActiveRecord descendants!
     *
     * @param string $className active record class name.
     * @return TemplateConfiguration the static model class
     */
    public static function model($className = __CLASS__)
    {
        /** @var self $model */
        $model = parent::model($className);
        return $model;
    }


    /**
     * Create a new entry in {{templates}} and {{template_configuration}} table using the template manifest
     *
     * @param string $sTemplateName the name of the template to import
     * @param array $aDatas Data
     * @return boolean true on success | exception
     * @throws Exception
     */
    public static function importManifest($sTemplateName, $aDatas = array())
    {
        if (!empty($aDatas['extends'])) {
            $oMotherTemplate = self::getInstanceFromTemplateName($aDatas['extends']);
            if (is_a($oMotherTemplate, 'TemplateConfiguration')) {
                $aDatas['api_version']     = $oMotherTemplate->template->api_version;
                $aDatas['view_folder']     = $oMotherTemplate->template->view_folder;
                $aDatas['author_email']    = $oMotherTemplate->template->author_email;
                $aDatas['author_url']      = $oMotherTemplate->template->author_url;
                $aDatas['copyright']       = $oMotherTemplate->template->copyright;
                $aDatas['version']         = $oMotherTemplate->template->version;
                $aDatas['license']         = $oMotherTemplate->template->license;
                $aDatas['files_folder']    = $oMotherTemplate->template->files_folder;
                $aDatas['aOptions']        = (empty($aDatas['aOptions']))
                    ? json_decode($oMotherTemplate->options)
                    : $aDatas['aOptions'];
            }
        }

        return parent::importManifest($sTemplateName, $aDatas);
    }

    /**
     * @todo document me
     */
    public function setToInherit()
    {
        $this->files_css         = 'inherit';
        $this->files_js          = 'inherit';
        $this->files_print_css   = 'inherit';
        $this->options           = 'inherit';
        $this->cssframework_name = 'inherit';
        $this->cssframework_css  = 'inherit';
        $this->cssframework_js   = 'inherit';
        $this->packages_to_load  = 'inherit';
    }

    /**
     * Check if the template exists and is valid
     *
     * @return bool
     */
    public function checkTemplate()
    {
        if (empty($this->bTemplateCheckResult)) {
            $this->bTemplateCheckResult = true;
            if (
                !is_object($this->template) ||
                (is_object($this->template) && !Template::checkTemplateXML($this->template))
            ) {
                $this->bTemplateCheckResult = false;
            }
        }
        return $this->bTemplateCheckResult;
    }

    /**
     * Sets bUseMagicInherit sTemplate isStandard and path of the theme
     *
     * @param string $sTemplateName
     * @param string $iSurveyId
     * @param bool $bUseMagicInherit
     */
    public function setBasics($sTemplateName = '', $iSurveyId = '', $bUseMagicInherit = false)
    {
        $this->bUseMagicInherit = $bUseMagicInherit;
        $this->sTemplateName = $this->template->name;
        $this->setIsStandard(); // Check if  it is a CORE template
        $this->path = ($this->isStandard)
            ? App()->getConfig("standardthemerootdir") . DIRECTORY_SEPARATOR . $this->template->folder . DIRECTORY_SEPARATOR
            : App()->getConfig("userthemerootdir") . DIRECTORY_SEPARATOR . $this->template->folder . DIRECTORY_SEPARATOR;
    }

    /**
     * Add a file replacement in the field `file_{css|js|print_css}` in table {{template_configuration}},
     * eg: {"replace": [ {original files to replace here...}, "css/template.css",]}
     * In general, should be called from TemplateManifest, after adding a file replacement inside the manifest.
     *
     * @param string $sFile the file to replace
     * @param string $sType css|js
     * @return bool|void
     * @throws Exception
     */
    public function addFileReplacement($sFile, $sType)
    {
        $sField = 'files_' . $sType;
        $oFiles = json_decode((string) $this->$sField, true);

        $oFiles['replace'][] = $sFile;

        $this->$sField = json_encode($oFiles);

        if ($this->save()) {
            return true;
        } else {
            throw new Exception("could not add $sFile to  $sField replacements! " . $this->getErrors());
        }
    }

    /**
     * @todo document me
     *
     * @return string
     */
    public function getTypeIcon()
    {
        if (empty($this->sTypeIcon)) {
            Yii::import('application.helpers.SurveyThemeHelper');
            $this->sTypeIcon = (SurveyThemeHelper::isStandardTemplate($this->template->name)) ?
                gT("Core theme") :
                gT("User theme");
        }
        return $this->sTypeIcon;
    }

    /**
     * Displays survey theme action buttons
     *
     * @return string
     */
    public function getButtons()
    {
        /* What ? We can get but $this->getAttribute ??? */
        $gsid = App()->request->getQuery('id', null); // $this->gsid;
        // don't show any buttons if user doesn't have update permission
        if (!Permission::model()->hasGlobalPermission('templates', 'update')) {
            /* Global settings */
            if (empty($gsid) || App()->getController()->action->id != "surveysgroups") {
                return '';
            }
            /* SurveysGroups settings */
            $oSurveysGroups = SurveysGroups::model()->findByPk($gsid);
            if (empty($oSurveysGroups)) {
                return '';
            }
            if (!$oSurveysGroups->hasPermission('surveys', 'update')) {
                return '';
            }
        }
        /* Use sanitized filename for previous bad upload */
        $templateName = sanitize_filename($this->template_name, false, false, false);
        $sEditorUrl = App()->getController()->createUrl(
            'admin/themes/sa/view',
            array("templatename" => $templateName)
        );
        $sExtendUrl = App()->getController()->createUrl('admin/themes/sa/templatecopy');
        $sOptionUrl = (App()->getController()->action->id == "surveysgroups") ?
            App()->getController()->createUrl(
                'themeOptions/updateSurveyGroup',
                array("id" => $this->id, "gsid" => $gsid)
            ) :
            App()->getController()->createUrl(
                'themeOptions/update',
                array("id" => $this->id)
            );

        $sUninstallUrl = Yii::app()->getController()->createUrl('themeOptions/uninstall/');
        $sResetUrl     = Yii::app()->getController()->createUrl('themeOptions/reset/', array("gsid" => (int) $gsid));

        $dropdownItems = [];
        $dropdownItems[] = [
            'title'            => gT('Theme editor'),
            'url'              => $sEditorUrl,
            'linkId'           => 'template_editor_link_' . $this->id,
            'linkClass'        => '',
            'iconClass'        => 'ri-brush-fill',
            'enabledCondition' => App()->getController()->action->id !== "surveysgroups",

        ];

        $dropdownItems[] = [
            'title'            => gT('Theme options'),
            'url'              => $sOptionUrl,
            'linkId'           => 'template_options_link_' . $this->id ,
            'linkClass'        => '',
            'iconClass'        => 'ri-dashboard-3-fill',
            'enabledCondition' => $this->getHasOptionPage(),
        ];


        $dropdownItems[] = [
            'title'            => gT('Extend'),
            'url'              => $sExtendUrl,
            'linkId'           => 'extendthis_' . $this->id,
            'linkClass'        => 'selector--ConfirmModal ',
            'iconClass'        => 'ri-file-copy-line text-success',
            'enabledCondition' => App()->getController()->action->id !== "surveysgroups",
            'linkAttributes'   => [
                'title'            => sprintf(gT('Type in the new name to extend %s'), $templateName),
                'data-button-no'   => gT('Cancel'),
                'data-button-yes'  => gT('Extend'),
                'data-text'        => gT('Please type in the new theme name above.'),
                'data-post'        => json_encode([
                    "copydir" => $templateName,
                    "action"  => "templatecopy",
                    "newname" => [ "value" => "extends_" . $templateName,
                                    "type" => "text",
                                    "class" => "form-control col-md-12" ]
                    ]),
            ]
        ];


        $dropdownItems[] = [
            'title'            => gT('Uninstall'),
            'url'              => $sUninstallUrl,
            'linkId'           => 'remove_fromdb_link_' . $this->id,
            'linkClass'        => 'selector--ConfirmModal ',
            'iconClass'        => 'ri-delete-bin-fill text-danger',
            'enabledCondition' => App()->getController()->action->id !== "surveysgroups" &&
                                    $templateName != App()->getConfig('defaulttheme'),
            'linkAttributes'   => [
                'title'            => gT('Uninstall this theme'),
                'data-button-no'   => gT('Cancel'),
                'data-button-yes'  => gT('Uninstall'),
                'data-text'        => gT('This will reset all the specific configurations of this theme.')
                                         . '<br>' . gT('Do you want to continue?'),
                'data-post'        => json_encode([ "templatename" => $templateName ]),
                'data-button-type' => "btn-danger"
            ]
        ];

        $dropdownItems[] = [
            'title'            => gT('Reset'),
            'url'              => $sResetUrl,
            'linkId'           => 'remove_fromdb_link_' . $this->id,
            'linkClass'        => 'selector--ConfirmModal ',
            'iconClass'        => 'ri-refresh-line text-warning',
            'enabledCondition' => App()->getController()->action->id !== "surveysgroups",
            'linkAttributes'   => [
                'title'            => gT('Reset this theme'),
                'data-button-no'   => gT('Cancel'),
                'data-button-yes'  => gT('Reset'),
                'data-text'        => gT('This will reload the configuration file of this theme.') . '<br>' . gT('Do you want to continue?'),
                'data-post'        => json_encode([ "templatename" => $templateName ]),
                'data-button-type' => "btn-warning"
            ]
        ];
        return App()->getController()->widget('ext.admin.grid.GridActionsWidget.GridActionsWidget', ['dropdownItems' => $dropdownItems], true);
    }

    /**
     * Returns true if this theme or any mothertemplate has a TemplateConfiguration set
     *
     * @return bool
     * @throws Exception
     */
    public function getHasOptionPage()
    {
        $filteredName = Template::templateNameFilter($this->template->name);
        $oRTemplate = $this->prepareTemplateRendering($filteredName);

        $sOptionFile = 'options' . DIRECTORY_SEPARATOR . 'options.twig';
        while (!file_exists($oRTemplate->path . $sOptionFile)) {
            $oMotherTemplate = $oRTemplate->oMotherTemplate;
            if (!($oMotherTemplate instanceof TemplateConfiguration)) {
                return false;
                break;
            }
            $oRTemplate = $oMotherTemplate->prepareTemplateRendering($this->template->name);
        }
        return true;
    }

    /**
     * Set a value on a given option at global setting level (survey level not affected).
     * Will be used to turn ON ajax mode on update.
     *
     * @param string $name
     * @param mixed $value
     * @return void
     */
    public function setGlobalOption($name, $value)
    {
        if ($this->options != 'inherit') {
            $oOptions = json_decode($this->options);

            if (empty($this->sid)) {
                $oOptions->$name = $value;
                $sOptions = json_encode($oOptions);
                $this->options = $sOptions;
                $this->save();
            }
        }
    }

    /**
     * Apply options from XML configuration for all missing template options
     *
     * @return void
     */
    public function addOptionFromXMLToLiveTheme()
    {
        if ($this->options != 'inherit') {
            $oOptions = get_object_vars(json_decode($this->options));
            $oTemplateConfigurationModel = new TemplateManifest();
            $oTemplateConfigurationModel->setBasics();
            $oXmlOptions = get_object_vars($oTemplateConfigurationModel->config->options);

            // compare template options to options from the XML and add if missing
            foreach ($oXmlOptions as $key => $value) {
                if (!array_key_exists($key, $oOptions)) {
                    $this->addOptionToLiveTheme($key, $value);
                }
            }
        }
    }

    /**
     * Add an option definition to the current theme.
     * Will be used to turn ON ajax mode on update.
     *
     * @param string $name
     * @param mixed $value
     * @return void
     */
    public function addOptionToLiveTheme($name, $value)
    {
        if ($this->options != 'inherit') {
            $oOptions = json_decode($this->options);
            $oOptions->$name = $value;
            $sOptions = json_encode($oOptions);
            $this->options = $sOptions;
            $this->save();
        }
    }

    /**
     * Set option (unless if options is set to "inherit").
     * @param string $name
     * @param mixed $value
     * @return void
     */
    public function setOption($name, $value)
    {
        if ($this->options != 'inherit') {
            $oOptions = json_decode($this->options);

            $oOptions->$name = $value;
            $sOptions = json_encode($oOptions);
            $this->options = $sOptions;
            $this->save();
        }
    }

    /**
     * @todo document me
     *
     * @param $from
     * @param $to
     * @return string
     */
    private function getRelativePath($from, $to)
    {
        $dir = explode(DIRECTORY_SEPARATOR, is_file($from) ? dirname((string) $from) : rtrim((string) $from, DIRECTORY_SEPARATOR));
        $file = explode(DIRECTORY_SEPARATOR, (string) $to);

        while ($dir && $file && ($dir[0] == $file[0])) {
            array_shift($dir);
            array_shift($file);
        }
        return str_repeat('..' . DIRECTORY_SEPARATOR, count($dir)) . implode(DIRECTORY_SEPARATOR, $file);
    }

    /**
     * Return image information
     *
     * @param string $file with Path
     * @return array|null
     */
    private function getImageInfo($file, $pathPrefix = '')
    {
        if (!file_exists($file)) {
            return;
        }
        // Currently it's private and only used one time, before put this function in twig :
        // must validate directory is inside rootdir
        $checkImage = LSYii_ImageValidator::validateImage($file);
        if (!$checkImage['check']) {
            return;
        }
        $filePath = $this->getRelativePath(App()->getConfig('rootdir'), $file);
        $previewFilePath = App()->getAssetManager()->publish($file);
        $fileName = basename($file);
        return [
            'preview' => $previewFilePath,
            'filepath' => $pathPrefix . $fileName,
            'filepathOptions' => $filePath,
            'filename' => $fileName
        ];
    }

    /**
     * @todo document me
     *
     * @return array
     */
    public function getOptionPageAttributes()
    {
        $aData = $this->attributes;
        $aData['maxFileSize'] = getMaximumFileUploadSize();
        $aData['imageFileList'] = [];
        Yii::import('application.helpers.SurveyThemeHelper');
        $categoryList = SurveyThemeHelper::getFileCategories($this->template_name, $this->sid);

        // Compose list of image files for each category
        foreach ($categoryList as $category) {
            // Get base path for category
            $pathPrefix = empty($category->pathPrefix) ? '' : $category->pathPrefix;
            $basePath = $category->path;
            // If the category is theme, add the "files folder" to the base path, as that's the directory to scan for files
            if ($category->name == 'theme') {
                $filesFolder = $this->getAttributeValue('files_folder') . DIRECTORY_SEPARATOR;
                $basePath = $basePath . $filesFolder;
                $pathPrefix = $pathPrefix . $filesFolder;
            }
            // Get full list of files
            $fileList = Template::getOtherFiles($basePath);
            // Order File List alphabetically
            usort($fileList, function ($a, $b) {
                return strcasecmp((string) $a['name'], (string) $b['name']);
            });
            // Keep only image files
            foreach ($fileList as $file) {
                $imageInfo = $this->getImageInfo($basePath . $file['name'], $pathPrefix);
                if ($imageInfo) {
                    $aData['imageFileList'][$imageInfo['filepath']] = array_merge(
                        [
                            'group' => $category->title,
                        ],
                        $imageInfo
                    );
                }
            };
        }

        return $aData;
    }

    /**
     * Prepares the rendering of the custom options.js and options.twig that can be used in every theme
     *
     * @return mixed
     */
    public function getOptionPage()
    {
        $oSimpleInheritance = Template::getInstance($this->template->name, $this->sid, $this->gsid, null, true);
        $oSimpleInheritance->options = 'inherit';
        $oSimpleInheritanceTemplate = $oSimpleInheritance->prepareTemplateRendering($this->template->name);

        // TODO: It's not clear which class prepareTemplateRendering() returns or should return.
        /** @var Template */
        $oTemplate = $this->prepareTemplateRendering($this->template->name);

        $renderArray = array('templateConfiguration' => $oTemplate->getOptionPageAttributes());

        $oTemplate->setToInherit();
        $oTemplate->setOptions();

        $oOptions = (array) $oSimpleInheritanceTemplate->oOptions;

        //We add some extra values to the option page
        //This is just a dirty hack, and somewhere in the future we will correct it
        $renderArray['oParentOptions'] = array_merge(
            ($oOptions),
            array(
                'packages_to_load' =>  $oTemplate->packages_to_load,
                'files_css' => $oTemplate->files_css
            )
        );

        $renderArray['aOptionAttributes'] = TemplateManifest::getOptionAttributes($oSimpleInheritance->path);
        $renderArray['aFontOptions'] = TemplateManifest::getFontDropdownOptions();
        return App()->twigRenderer->renderOptionPage($oTemplate, $renderArray);
    }

    /**
     * From a list of json files in db it will generate a PHP array ready to use by removeFileFromPackage()
     *
     * @param TemplateConfiguration $oTemplate
     * @param string $sType
     * @param string $sAction Action
     * @return array
     * @internal param string $jFiles json
     */
    protected function getFilesTo($oTemplate, $sType, $sAction)
    {
        // Todo: make it in a recursive way
        if (!empty($this->aFilesTo[$oTemplate->template->name])) {
            if (!empty($this->aFilesTo[$oTemplate->template->name][$sType])) {
                if (!empty($this->aFilesTo[$oTemplate->template->name][$sType][$sAction])) {
                    return $this->aFilesTo[$oTemplate->template->name][$sType][$sAction];
                } else {
                    $this->aFilesTo[$oTemplate->template->name][$sType][$sAction] = array();
                }
            } else {
                $this->aFilesTo[$oTemplate->template->name][$sType]           = array();
                $this->aFilesTo[$oTemplate->template->name][$sType][$sAction] = array();
            }
        } else {
            $this->aFilesTo[$oTemplate->template->name]                   = array();
            $this->aFilesTo[$oTemplate->template->name][$sType]           = array();
            $this->aFilesTo[$oTemplate->template->name][$sType][$sAction] = array();
        }

        $sField = 'files_' . $sType;
        $oFiles = $this->getOfiles($oTemplate, $sField);

        $aFiles = array();

        if ($oFiles) {
            foreach ($oFiles as $action => $aFileList) {
                if (is_array($aFileList)) {
                    if ($action == $sAction) {
                        // Specific inheritance of one of the value of the json array
                        if ($aFileList[0] == 'inherit') {
                            $aParentjFiles = json_decode((string) $oTemplate->getParentConfiguration->$sField, true);
                            $aFileList = $aParentjFiles[$action];
                        }

                        $aFiles = array_merge($aFiles, $aFileList);
                    }
                }
            }
        }

        $this->aFilesTo[$oTemplate->template->name][$sType][$sAction] = $aFiles;
        return $aFiles;
    }

    /**
     * Get the json files (to load/replace/remove) from  a theme, and checks if its correctly formated
     *
     * @param $oTemplate the theme to check
     * @param $sField name of the DB field to get (file_css, file_js, file_print_css)
     * @return bool|mixed
     */
    protected function getOfiles($oTemplate, $sField)
    {
        if (!empty($this->Ofiles[$oTemplate->template->name])) {
            if (!empty($this->Ofiles[$oTemplate->template->name][$sField])) {
                return $this->Ofiles[$oTemplate->template->name][$sField];
            } else {
                $this->Ofiles[$oTemplate->template->name][$sField] = array();
            }
        } else {
            $this->Ofiles[$oTemplate->template->name] = array();
            $this->Ofiles[$oTemplate->template->name][$sField] = array();
        }

        $files = $oTemplate->$sField;
        $oFiles = [];
        if (!empty($files)) {
            $oFiles = json_decode((string) $files, true);
            if ($oFiles === null) {
                App()->setFlashMessage(
                    sprintf(
                        gT('Error: Malformed JSON - field %s must be either a JSON array or the string "inherit". Found "null".'),
                        $sField
                    ),
                    'error'
                );
                return false;
            }
        }

        $this->Ofiles[$oTemplate->template->name][$sField] = $oFiles;
        return $oFiles;
    }

    /**
     * Proxy for Yii::app()->clientScript->removeFileFromPackage()
     *
     * @param string $sPackageName name of the package to edit
     * @param string $sType        the type of settings to change (css or js)
     * @param $aSettings           array of local setting
     * @return void
     */
    protected function removeFileFromPackage($sPackageName, $sType, $aSettings)
    {
        foreach ($aSettings as $sFile) {
            App()->clientScript->removeFileFromPackage($sPackageName, $sType, $sFile);
        }
    }

    /**
     * Configure the mother template (and its mother templates)
     * This is an object recursive call to TemplateConfiguration::prepareTemplateRendering()
     */
    protected function setMotherTemplates()
    {
        if (!empty($this->template->extends)) {
            $sMotherTemplateName   = $this->template->extends;
            $instance = TemplateConfiguration::getInstanceFromTemplateName($sMotherTemplateName);
            $instance->template->checkTemplate();
            $this->oMotherTemplate = $instance->prepareTemplateRendering($sMotherTemplateName, '');
        }
    }

    /**
     * Get the closest template in the hierarchy that has the definition for $attribute
     *
     * @param TemplateConfiguration $oRTemplate
     * @param string $attribute
     * @return TemplateConfiguration
     */
    protected function getTemplateConfigurationForAttribute($oRTemplate, $attribute)
    {
        while (empty($oRTemplate->getRelatedTemplate()->$attribute)) {
            $oMotherTemplate = $oRTemplate->oMotherTemplate;
            if (!($oMotherTemplate instanceof TemplateConfiguration)) {
                $this->uninstallIncorectTheme($this->template_name);
                break;
            }
            $oRTemplate = $oMotherTemplate;
        }
        return $oRTemplate;
    }

    /**
     * Uninstall a theme and, display error message, and redirect to theme list
     * @param string $sTemplateName
     */
    protected function uninstallIncorectTheme($sTemplateName)
    {
        TemplateConfiguration::uninstall($sTemplateName);
        App()->setFlashMessage(
            sprintf(
                gT("Theme '%s' has been uninstalled because it's not compatible with this LimeSurvey version."),
                $sTemplateName
            ),
            'error'
        );
        App()->getController()->redirect(array("themeOptions/index", "#" => "surveythemes"));
        App()->end();
    }

    /**
     * Set the default configuration values for the template, and use the motherTemplate value if needed
     *
     * @return void
     */
    protected function setThisTemplate()
    {
        // Mandatory setting in config XML
        $this->apiVersion = (!empty($this->template->api_version)) ? $this->template->api_version : null;
        $this->viewPath = $this->path . $this->getTemplateConfigurationForAttribute($this, 'view_folder')->template->view_folder . DIRECTORY_SEPARATOR;
        $this->filesPath = $this->path . $this->getTemplateConfigurationForAttribute($this, 'files_folder')->template->files_folder . DIRECTORY_SEPARATOR;
        $this->generalFilesPath = App()->getConfig("userthemerootdir") . DIRECTORY_SEPARATOR . 'generalfiles' . DIRECTORY_SEPARATOR;
        // Options are optional
        $this->setOptions();

        // Not mandatory (use package dependances)
        $this->setCssFramework();
        $this->packages = $this->getDependsPackages($this);
        if (!empty($this->packages_to_load)) {
            $templateToLoadPackages = json_decode($this->packages_to_load);
            if (!empty($templateToLoadPackages->add)) {
                $this->packages = array_merge($templateToLoadPackages->add, $this->packages);
            }
            if (!empty($templateToLoadPackages->remove)) {
                $this->packages = array_diff($this->packages, $templateToLoadPackages->remove);
            }
        }

        // Add depend package according to packages
        $this->depends = array_merge($this->depends, $this->packages);
    }

    /**
     * @todo document me
     * @return void
     */
    private function setCssFramework()
    {
        if (!empty($this->cssframework_name)) {
            $this->cssFramework = new \stdClass();
            $this->cssFramework->name = $this->cssframework_name;
            $this->cssFramework->css  = json_decode($this->cssframework_css);
            $this->cssFramework->js   = json_decode($this->cssframework_js);
        } else {
            $this->cssFramework = new \stdClass();
            $this->cssFramework->name = '';
            $this->cssFramework->css  = '';
            $this->cssFramework->js   = '';
        }
    }

    /**
     * Decodes json string from the database field "options" and stores it inside $this->oOptions
     * Also triggers inheritence checks
     * @return void
     */
    protected function setOptions()
    {
        $this->oOptions = new stdClass();
        if (!empty($this->options)) {
            $this->oOptions = json_decode($this->options);
        }
        // unset "comment" property which is auto generated from HTML comments in xml file
        unset($this->oOptions->comment);

        $this->setOptionInheritance();
    }

    /**
     * Loop through all theme options defined, trigger check for inheritance and write the new value back to the options object
     * @return void
     */
    protected function setOptionInheritance()
    {
        $oOptions = $this->oOptions;

        if (!empty($oOptions)) {
            foreach ($oOptions as $sKey => $sOption) {
                $this->oOptions->$sKey = $this->getOptionKey($sKey);
            }
        }
    }

    /**
     * Search through the inheritence chain and find the inherited value for theme option
     * @param string $key
     * @return mixed
     */
    protected function getOptionKey($key)
    {
        $aOptions = json_decode($this->options, true);
        if (isset($aOptions[$key])) {
            $value = $aOptions[$key];
            if ($value === 'inherit') {
                $oParentConfig = $this->getParentConfiguration();
                if ($oParentConfig->id != $this->id) {
                    return $this->getParentConfiguration()->getOptionKey($key);
                } else {
                    $this->uninstallIncorectTheme($this->template_name);
                }
            }
            return  $value;
        } else {
            return null;
        }
    }

    /**
     * loads the main theme template from the parent theme that it is extending, as a package. Ready to be registered
     *
     * @param string[] $packages
     * @return string[]
     */
    protected function addMotherTemplatePackage($packages)
    {
        if (!empty($this->template->extends)) {
            $sMotherTemplateName = (string) $this->template->extends;
            $packages[]          = 'survey-template-' . $sMotherTemplateName;
        }
        return $packages;
    }

    /**
     * Get the list of file replacement from Engine Framework
     * @param string  $sType            css|js the type of file
     * @param boolean $bInlcudeRemove   also get the files to remove
     * @return array
     */
    protected function getFrameworkAssetsToReplace($sType, $bInlcudeRemove = false)
    {
        if (empty($this->aFrameworkAssetsToReplace)) {
            $this->aFrameworkAssetsToReplace = array();
        }

        $this->aFrameworkAssetsToReplace[$sType] = array();

        $sFieldName  = 'cssframework_' . $sType;
        $aFieldValue = json_decode((string) $this->$sFieldName, true);

        if (!empty($aFieldValue) && !empty($aFieldValue['replace'])) {
            $this->aFrameworkAssetsToReplace[$sType] = (array) $aFieldValue['replace'];

            // Inner field inheritance
            foreach ($this->aFrameworkAssetsToReplace[$sType] as $key => $aFiles) {
                foreach ($aFiles as $sReplacement) {
                    if ($sReplacement == "inherit") {
                        $aParentReplacement = $this->getParentConfiguration()->getFrameworkAssetsToReplace($sType);
                        $this->aFrameworkAssetsToReplace[$sType][$key][1] = $aParentReplacement[$key][1];
                    }
                }
            }

            if ($bInlcudeRemove && isset($aFieldValue['remove'])) {
                $this->aFrameworkAssetsToReplace[$sType] = array_merge(
                    $this->aFrameworkAssetsToReplace,
                    (array) $aFieldValue['remove']
                );
            }
        }

        return $this->aFrameworkAssetsToReplace[$sType];
    }

    /**
     * Get the list of file replacement from Engine Framework
     * @param string  $sType            css|js the type of file
     * @return array
     */
    protected function getFrameworkAssetsReplacement($sType)
    {
        if (empty($this->aReplacements)) {
            $this->aReplacements = array();
        }
        $this->aReplacements[$sType] = array();

        $aFrameworkAssetsToReplace = $this->getFrameworkAssetsToReplace($sType);

        foreach ($aFrameworkAssetsToReplace as $key => $aAsset) {
            $aReplace = $aAsset[1];
            $this->aReplacements[$sType][] = $aReplace;
        }


        return $this->aReplacements[$sType];
    }

    /**
     * @todo document me
     * @return TemplateConfiguration
     */
    public function getParentConfiguration()
    {
        if (empty($this->oParentTemplate)) {
            //check for surveygroup id if a survey is given
            if ($this->sid != null) {
                $oSurvey = Survey::model()->findByPk($this->sid);
                // set template name from real inherited value
                $sTemplateName = !empty($oSurvey->oOptions->template) ?
                    $oSurvey->oOptions->template :
                    $this->template->name;
                $oParentTemplate = Template::getTemplateConfiguration($sTemplateName, null, $oSurvey->gsid);
                if (is_a($oParentTemplate, 'TemplateConfiguration')) {
                    $this->oParentTemplate = $oParentTemplate;
                    $this->oParentTemplate->bUseMagicInherit = $this->bUseMagicInherit;
                    return $this->oParentTemplate;
                }
            }

            //check for surveygroup id if a surveygroup is given
            if ($this->sid == null && $this->gsid != null) {
                $oSurveyGroup = SurveysGroups::model()->findByPk($this->gsid);
                //Switch if the surveygroup inherits from a parent surveygroup
                if ($oSurveyGroup != null && $oSurveyGroup->parent_id != 0) {
                    $oParentTemplate = Template::getTemplateConfiguration(
                        $this->template->name,
                        null,
                        $oSurveyGroup->parent_id
                    );
                    if (is_a($oParentTemplate, 'TemplateConfiguration')) {
                        $this->oParentTemplate = $oParentTemplate;
                        $this->oParentTemplate->bUseMagicInherit = $this->bUseMagicInherit;
                        return $this->oParentTemplate;
                    }
                }
            }

            //in the endcheck for general global template
            $this->oParentTemplate = Template::getTemplateConfiguration($this->template_name, null, null);
            $this->oParentTemplate->bUseMagicInherit = $this->bUseMagicInherit;
            return $this->oParentTemplate;
        }
        return $this->oParentTemplate;
    }


    /**
     * Change the template name inside the configuration entries (called from template editor)
     * NOTE: all tests (like template exist, etc) are done from template controller.
     *
     * @param string $sOldName The old name of the template
     * @param string $sNewName The newname of the template
     */
    public static function rename($sOldName, $sNewName)
    {
        self::model()->updateAll(
            array('template_name' => $sNewName),
            "template_name = :oldname",
            array(':oldname' => $sOldName)
        );
    }

    /**
     * Proxy for the AR method to manage the inheritance
     * If one of the field that can be inherited is set to "inherit", then it will return the value of its parent
     * NOTE: this is recursive, if the parent field itself is set to inherit, then it will
     * the value of the parent of the parent, etc
     *
     * @param string $name the name of the attribute
     * @return mixed
     */
    public function __get($name)
    {
        $aAttributesThatCanBeInherited = array(
            'files_css',
            'files_js',
            'options',
            'cssframework_name',
            'cssframework_css',
            'cssframework_js',
            'packages_to_load'
        );

        if (in_array($name, $aAttributesThatCanBeInherited) && $this->bUseMagicInherit) {
            // Full inheritance of the whole field
            $sAttribute = parent::__get($name);
            if ($sAttribute === 'inherit') {
                // NOTE: this is object recursive (if parent configuration field is set to inherit,
                // then it will lead to this method again.)
                $oParentConfiguration = $this->getParentConfiguration();
                /**
                 * We check if $oParentConfiguration is the same as $this because if it is, $oParentConfiguration->$name will
                 * try to directly access the property instead of calling the magic method, and it will fail for dynamic properties.
                 * @todo: Review the behavior of getParentConfiguration(). Returning the same object seems to be a bug.
                 */
                if ($oParentConfiguration !== $this) {
                    $sAttribute = $oParentConfiguration->$name;
                } else {
                    $sAttribute = $oParentConfiguration->getAttribute($name);
                }
            }
        } else {
            $sAttribute = parent::__get($name);
        }

        return $sAttribute;
    }

    /**
     * @todo document me
     * @return string
     */
    public function getTemplateAndMotherNames()
    {
        $oRTemplate = $this;
        $sTemplateNames = $this->sTemplateName;

        while (!empty($oRTemplate->oMotherTemplate)) {
            $sTemplateNames .= ' ' . $oRTemplate->template->extends;
            $oRTemplate      = $oRTemplate->oMotherTemplate;
            if (!($oRTemplate instanceof TemplateConfiguration)) {
                // Throw alert: should not happen
                break;
            }
        }

        return $sTemplateNames;
    }

    /**
     * Get the global template configuration with same name as $this.
     * The global config has no sid, no gsid and no uid.
     * @return TemplateConfiguration
     */
    public function getGlobalParent()
    {
        return self::model()->find(
            'sid IS NULL AND uid IS NULL and gsid IS NULL AND template_name = :template_name',
            [':template_name' => $this->template_name]
        );
    }

    /**
     * Get showpopups value from config or template configuration
     */
    public function getshowpopups()
    {
        $config = (int) App()->getConfig('showpopups');
        if ($config == 2) {
            if (isset($this->oOptions->showpopups)) {
                $this->showpopups = (int)$this->oOptions->showpopups;
            } else {
                $this->showpopups = 1;
            }
        } else {
            $this->showpopups = $config;
        }
    }

    /**
     * Set each option key value to 'inherit' instead of having only one 'inherit' value for options.
     * Keys are fetched from parent xml configuration.
     */
    public function setOptionKeysToInherit()
    {
        $oTemplate = $this->getParentConfiguration();
        $oTemplate->bUseMagicInherit = true;
        $oTemplate->setOptions();

        $aOptions = array();
        if ((string) $this->options === 'inherit') {
            foreach ($oTemplate->oOptions as $key => $value) {
                $aOptions[$key] = 'inherit';
            }
            $this->options = json_encode($aOptions);
        }
    }

    /**
     * Sanitizes the theme options making sure that paths are valid.
     * Options that match a file will be marked as invalid if the file
     * is not valid, or replaced with the virtual path if the file is valid.
     */
    public function sanitizeImagePathsOnJson($attribute, $params)
    {
        $excludedOptions = [
            'cssframework'
        ];
        // Validates all options of the theme. Not only classic ones which are expected to hold a path,
        // as other options may hold a path as well (eg. custom theme options)
        $decodedOptions = json_decode((string) $this->$attribute, true);
        if (is_array($decodedOptions)) {
            Yii::import('application.helpers.SurveyThemeHelper');
            foreach ($decodedOptions as $option => &$value) {
                if (in_array($option, $excludedOptions)) {
                    continue;
                }
                $value = SurveyThemeHelper::sanitizePathInOption($value, $this->template_name, $this->sid);
            }
            $this->$attribute = json_encode($decodedOptions);
        }
    }

    /**
     * Returns the related Template.
     * The template can only be accessed as a relation when this model is stored in the DB. Before
     * saving, $this->template is null. In that case, this method will load the approriate Template.
     * @return Template|null
     */
    private function getRelatedTemplate()
    {
        $template = !empty($this->template) ? $this->template : Template::model()->findByAttributes(['name' => $this->template_name]);
        return $template;
    }

    /**
     * Returns the value of the specified attribute ($attributeName) from
     * the closest Template in the hierarchy.
     *
     * @param string $attributeName
     * @return mixed
     */
    private function getAttributeValue($attributeName)
    {
        return $this->getTemplateConfigurationForAttribute($this, $attributeName)->template->$attributeName;
    }

    /**
     * Returns the html to render the previewimage of the template.
     * Pass $justTheUrl = true to return just the URL of the preview image
     * @param bool $justTheUrl
     * @return string
     */
    public function getPreview($justTheUrl = false)
    {
        $previewUrl = '';
        if (empty($this->sPreviewImgTag)) {
            if (is_a($this->template, 'Template')) {
                $sTemplateFileFolder = Template::getTemplatesFileFolder($this->template->name);
                $previewPath         = Template::getTemplatePath($this->template->name) . '/' . $sTemplateFileFolder;

                if ($previewPath && file_exists($previewPath . '/preview.png')) {
                    $previewUrl = Template::getTemplateURL($this->template->name) . $sTemplateFileFolder . '/preview.png';
                    $this->sPreviewImgTag = '<img src="' .
                        $previewUrl .
                        '" alt="template preview" height="200" class="img-thumbnail p-0 rounded-0" />';
                }
            } else {
                $this->sPreviewImgTag = '<em>' . gT('No preview available') . '</em>';
            }
        }

        return $justTheUrl ? $previewUrl : $this->sPreviewImgTag;
    }

    /**
     * Prepare all the needed datas to render the temple
     * If any problem (like template doesn't exist), it will load the default theme configuration
     * NOTE 1: This function will create/update all the packages needed to render the template, which imply to do the
     *         same for all mother templates
     * NOTE 2: So if you just want to access the TemplateConfiguration AR Object, you don't need to call it. Call it
     *         only before rendering anything related to the template.
     *
     * @param  string $sTemplateName the name of the template to load.
     *                               The string comes from the template selector in survey settings
     * @param  string $iSurveyId the id of the survey. If
     * @param bool $bUseMagicInherit
     * @return self
     */
    public function prepareTemplateRendering($sTemplateName = '', $iSurveyId = '', $bUseMagicInherit = true)
    {
        if (!empty($sTemplateName) && !empty($iSurveyId)) {
            if (!empty(self::$aPreparedToRender[$sTemplateName][$iSurveyId][$bUseMagicInherit])) {
                return self::$aPreparedToRender[$sTemplateName][$iSurveyId][$bUseMagicInherit];
            }
            self::$aPreparedToRender[$sTemplateName][$iSurveyId][$bUseMagicInherit] = [];
        }

        $this->setBasics($sTemplateName, $iSurveyId, $bUseMagicInherit);
        $this->setMotherTemplates(); // Recursive mother templates configuration
        $this->setThisTemplate(); // Set the main config values of this template
        $this->createTemplatePackage($this); // Create an asset package ready to be loaded
        $this->removeFiles();
        $this->getshowpopups();

        if (!empty($sTemplateName) && !empty($iSurveyId)) {
            self::$aPreparedToRender[$sTemplateName][$iSurveyId][$bUseMagicInherit] = $this;
        }
        return $this;
    }

    /**
     * Create a package for the asset manager.
     * The asset manager will push to tmp/assets/xyxyxy/ the whole template directory (with css, js, files, etc.)
     * And it will publish the CSS and the JS defined in config.xml. So CSS can use relative path for pictures.
     * The publication of the package itself is in LSETwigViewRenderer::renderTemplateFromString()
     *
     * @param TemplateConfiguration|TemplateManifest $oTemplate TemplateManifest
     */
    protected function createTemplatePackage($oTemplate)
    {
        // Each template in the inheritance tree needs a specific alias
        $sPathName  = 'survey.template-' . $oTemplate->sTemplateName . '.path';
        $sViewName  = 'survey.template-' . $oTemplate->sTemplateName . '.viewpath';

        Yii::setPathOfAlias($sPathName, $oTemplate->path);
        Yii::setPathOfAlias($sViewName, $oTemplate->viewPath);

        // First we add the framework replacement (bootstrap.css must be loaded before template.css)
        $aCssFiles  = $this->getFrameworkAssetsReplacement('css');
        $aJsFiles   = $this->getFrameworkAssetsReplacement('js');

        // This variable will be used to add the variation name to the body class
        // via $aClassAndAttributes['class']['body']
        $this->aCssFrameworkReplacement = $aCssFiles;

        // Then we add the template config files
        $aTCssFiles = $this->getFilesToLoad($oTemplate, 'css');
        $aTJsFiles  = $this->getFilesToLoad($oTemplate, 'js');

        $aCssFiles  = array_merge($aCssFiles, $aTCssFiles);
        $aJsFiles   = array_merge($aJsFiles, $aTJsFiles);

        // Remove/Replace mother template files
        if (
            App()->getConfig('force_xmlsettings_for_survey_rendering') ||
            ($this->template instanceof Template &&  $this->template->extends) ||
            !empty($this->config->metadata->extends)
        ) {
              $aCssFiles = $this->changeMotherConfiguration('css', $aCssFiles);
              $aJsFiles  = $this->changeMotherConfiguration('js', $aJsFiles);
        }

        //For fruity_twentythree surveytheme we completely replace the variation theme css file:
        $aCssFiles = $this->replaceVariationFilesWithRtl($aCssFiles);

        $this->sPackageName = 'survey-template-' . $this->sTemplateName;
        $sTemplateurl       = $oTemplate->getTemplateURL();

        $aDepends = empty($oTemplate->depends) ? array() : $oTemplate->depends;

        // The package "survey-template-{sTemplateName}" will be available from anywhere in the app now.
        // To publish it : Yii::app()->clientScript->registerPackage( 'survey-template-{sTemplateName}' );
        // Depending on settings, it will create the asset directory, and publish the css and js files
        App()->clientScript->addPackage($this->sPackageName, array(
            'devBaseUrl'  => $sTemplateurl, // Used when asset manager is off
            'basePath'    => $sPathName, // Used when asset manager is on
            'css'         => $aCssFiles,
            'js'          => $aJsFiles,
            'depends'     => $aDepends,
        ));
    }

    /**
     * When rtl language is chosen:
     * if a css file in folder variations is in array cssFiles, then it will be replaced with the
     * *-rtl version
     * @param array $cssFiles
     * @return array
     */
    private function replaceVariationFilesWithRtl(array $cssFiles)
    {
        if (getLanguageRTL(App()->getLanguage()) == 'rtl') {
            foreach ($cssFiles as $index => $cssFile) {
                if (strpos($cssFile, 'css/variations/theme_') !== false) {
                    $cssFileSplitArray = explode('.', $cssFile);
                    $cssFiles[$index] =  $cssFileSplitArray[0] . '-rtl.css';
                }
            }
        }
        return $cssFiles;
    }
}