1<?php
2if (!defined('BASEPATH')) {
3    exit('No direct script access allowed');
4}
5/*
6* LimeSurvey
7* Copyright (C) 2007-2015 The LimeSurvey Project Team / Carsten Schmitz
8* All rights reserved.
9* License: GNU/GPL License v2 or later, see LICENSE.php
10* LimeSurvey is free software. This version may have been modified pursuant
11* to the GNU General Public License, and as distributed it includes or
12* is derivative of works licensed under the GNU General Public License or
13* other free or open source software licenses.
14* See COPYRIGHT.php for copyright notices and details.
15*/
16
17/**
18 * Template Configuration Model
19 *
20 * This model retrieves all the data of template configuration from the configuration file
21 *
22 * @package       LimeSurvey
23 * @subpackage    Backend
24 */
25class TemplateManifest extends TemplateConfiguration
26{
27    public $templateEditor;
28    public $sPreviewImgTag;
29
30    /* There is no option inheritance on Manifest mode: values from XML are always used. So no: $bUseMagicInherit */
31
32
33    /**
34     * Public interface specific to TemplateManifest
35     * They are used in TemplateEditor
36     */
37
38    /**
39     * Update the configuration file "last update" node.
40     * For now, it is called only from template editor
41     */
42    public function actualizeLastUpdate()
43    {
44        libxml_disable_entity_loader(false);
45        $config = simplexml_load_file(realpath($this->xmlFile));
46        $config->metadata->last_update = date("Y-m-d H:i:s");
47        $config->asXML(realpath($this->xmlFile)); // Belt
48        touch($this->path); // & Suspenders ;-)
49        libxml_disable_entity_loader(true);
50    }
51
52    /**
53     * Used from the template editor.
54     * It returns an array of editable files by screen for a given file type
55     *
56     * @param   string  $sType      the type of files (view/css/js)
57     * @param   string  $sScreen    the screen you want to retreive the files from. If null: all screens
58     * @return  array   array       ( [screen name] => array([files]) )
59     */
60    public function getValidScreenFiles($sType = "view", $sScreen = null)
61    {
62        $aScreenFiles = array();
63
64        if (empty($this->templateEditor)) {
65            return array();
66        }
67
68        $filesFromXML = (is_null($sScreen)) ? (array) $this->templateEditor->screens->xpath('//file') : $this->templateEditor->screens->xpath('//'.$sScreen.'/file');
69
70        foreach ($filesFromXML as $file) {
71            if ($file->attributes()->type == $sType) {
72                $aScreenFiles[] = (string) $file;
73            }
74        }
75
76        $oEvent = new PluginEvent('getValidScreenFiles');
77        $oEvent->set('type', $sType);
78        $oEvent->set('screen',$sScreen);
79        //$oEvent->set('files',$aScreenFiles); // Not needed since we have remove and add event
80        App()->getPluginManager()->dispatchEvent($oEvent);
81        $aScreenFiles = array_values(array_diff($aScreenFiles, (array) $oEvent->get('remove')));
82        $aScreenFiles = array_merge($aScreenFiles, (array)$oEvent->get('add'));
83        $aScreenFiles = array_unique($aScreenFiles);
84        return $aScreenFiles;
85    }
86
87    /**
88     * Returns the complete list of screens, with layout and contents. Used from Twig Command line
89     * @return array the list of screens, layouts, contents
90     */
91    public function getScreensDetails()
92    {
93      $aContent = array();
94
95      $oScreensFromXML = $this->templateEditor->xpath('//screens');
96      foreach ($oScreensFromXML[0] as $sScreen => $oScreen){
97
98        // We reset LayoutName and FileName at each loop to avoid errors
99        $sLayoutName = "";
100        $sFileName = "";
101        $sTitle = "";
102
103        foreach ($oScreen as $sKey => $oField){
104
105            if ($oField->attributes()->role == "layout") {
106              $sLayoutName  = (string) $oField;
107            }
108
109            if ($oField->attributes()->role == "content") {
110              $sFile  = (string) $oField;
111
112              // From command line, we need to remove the full path for content. It's inside the layout. This could be an option
113              $aFile     = explode("/", $sFile);
114              $aFileName = explode(".", end($aFile));
115              $sContent = $aFileName[0];
116            }
117
118            if ($oField->attributes()->role == "title") {
119              $sTitle  = (string) $oField;
120
121              if ($oField->attributes()->twig == "on") {
122                $sTitle = Yii::app()->twigRenderer->convertTwigToHtml($sTitle);
123              }
124            }
125
126        }
127
128        if (!empty ($sLayoutName)){
129          $aContent[$sScreen]['title'] = $sTitle;
130          $aContent[$sScreen]['layouts'][$sLayoutName] = $sContent;
131        }
132      }
133
134      return $aContent;
135    }
136
137    /**
138     * Returns an array of screens list with their respective titles. Used by Theme Editor to build the screend selection dropdown
139     * For retro-compatibility purpose, if the array is empty it will use the old default values.
140     *
141     * @return array the list of screens with their titles
142     */
143    public function getScreensList()
144    {
145      $aScreenList = $this->getScreensDetails();
146      $aScreens = array();
147
148      foreach($aScreenList as $sScreenName => $aTitleAndLayouts){
149        $aScreens[$sScreenName] = $aTitleAndLayouts['title'];
150      }
151
152      // We check there is at least one screen title in the array. Else, the theme manifest is outdated, so we use the default values
153      $bEmptyTitles = true;
154      foreach($aScreens as $sScreenName => $sTitle){
155        if (!empty($sTitle)){
156          $bEmptyTitles = false;
157          break;
158        }
159      }
160
161      if ($bEmptyTitles){
162          if(YII_DEBUG){
163            Yii::app()->setFlashMessage("Your theme does not implement screen definition in XML. Using the default ones <br> this message will not appear when debug mode is off", 'error');
164          }
165
166          $aScreens['welcome']         = gT('Welcome', 'unescaped');
167          $aScreens['question']        = gT('Question', 'unescaped');
168          $aScreens['completed']       = gT('Completed', 'unescaped');
169          $aScreens['clearall']        = gT('Clear all', 'unescaped');
170          $aScreens['load']            = gT('Load', 'unescaped');
171          $aScreens['save']            = gT('Save', 'unescaped');
172          $aScreens['surveylist']      = gT('Survey list', 'unescaped');
173          $aScreens['error']           = gT('Error', 'unescaped');
174          $aScreens['assessments']     = gT('Assessments', 'unescaped');
175          $aScreens['register']        = gT('Registration', 'unescaped');
176          $aScreens['printanswers']    = gT('Print answers', 'unescaped');
177          $aScreens['pdf']             = gT('PDF', 'unescaped');
178          $aScreens['navigation']      = gT('Navigation', 'unescaped');
179          $aScreens['misc']            = gT('Miscellaneous files', 'unescaped');
180      }
181
182      return $aScreens;
183
184    }
185
186    /**
187     * Return the default datas for theme views.
188     * This is used when rendering the views outside of the normal survey taking.
189     * Currently used in two cases: theme editor preview, and twig cache file generation from command line.
190     */
191    public function getDefaultDataForRendering($thissurvey=array())
192    {
193
194      $thissurvey    = empty($thissurvey)?$this->getDefaultCoreDataForRendering():$thissurvey;
195
196      $thissurvey = $this->getDefaultDataForRenderingFromXml($thissurvey);
197
198      //$thissurvey['alanguageChanger'] = $this->getDefaultDataForLanguageChanger();
199
200      // Redundant values
201      $thissurvey['surveyls_title'] = $thissurvey['name'];
202      $thissurvey['surveyls_description'] = $thissurvey['description'];
203      $thissurvey['surveyls_welcometext'] = $thissurvey['welcome'];
204
205      return $thissurvey;
206    }
207
208
209    public function getDefaultDataForLanguageChanger($thissurvey=array())
210    {
211
212      $thissurvey    = empty($thissurvey)?array():$thissurvey;
213      $oDataFromXML = $this->templateEditor->default_data->xpath('//survey_data');
214
215
216      $thissurvey['alanguageChanger']['datas'] = [
217                    'sSelected' => 'en',
218                    //'withForm' => true,  // Set to true for no-js functionality.
219                    'aListLang' => [
220                        'en' => gT('English'),
221                        'de' => gT('German')
222                    ]
223                ];
224
225
226    }
227
228    public function getDefaultDataForRenderingFromXml($thissurvey=array())
229    {
230        $thissurvey    = empty($thissurvey)?array():$thissurvey;
231
232        if (empty($this->templateEditor)) {
233            return $thissurvey;
234        }
235
236        $thissurvey = $this->parseDefaultData('survey', $thissurvey);
237        $thissurvey['aGroups'][1] = $this->parseDefaultData('group', $thissurvey['aGroups'][1]);
238        $thissurvey['aGroups'][1]["aQuestions"][1] = $this->parseDefaultData('question_1', $thissurvey['aGroups'][1]["aQuestions"][1]) ;
239        $thissurvey['aGroups'][1]["aQuestions"][2] = $this->parseDefaultData('question_2', $thissurvey['aGroups'][1]["aQuestions"][2]);
240        $thissurvey['aAssessments']["datas"]["total"][0] = $this->parseDefaultData('assessments', $thissurvey['aAssessments']["datas"]["total"][0]);
241
242        /**
243         * NOTE: This will allow Theme developper to add their new screens without editing this file.
244         * It implies they respect the convention :
245         * $aSurveyData[custom screen name][custom variable] = custom variable value
246         * Where custom variable value can't be an array.
247         * TODO: for LS5, refactor all the twig views and theme editor so we use only this convetion.
248         * Eg: don't use arrays like $thissurvey['aAssessments']["datas"]["total"][0] or $thissurvey['aGroups'][1]["aQuestions"][1]
249        */
250        $thissurvey = $this->getCustomScreenData($thissurvey);
251
252        return $thissurvey;
253    }
254
255    /**
256     * If theme developer created custom screens, they will provide custom data.
257     * This function will get those custom data to pass them to the preview.
258     */
259    protected function getCustomScreenData($thissurvey = array())
260    {
261      $oDataFromXML = $this->templateEditor->xpath("//default_data"); //
262
263      foreach( $oDataFromXML[0] as $sScreenName => $oData){
264        if ($oData->attributes()->type == "custom"){
265          $sArrayName = (string) $oData->attributes()->arrayName;
266          $thissurvey[$sArrayName] = array();
267          $thissurvey[$sArrayName] = $this->parseDefaultData($sScreenName, $thissurvey[$sArrayName]);
268        }
269      }
270
271      return $thissurvey;
272    }
273
274
275    protected function parseDefaultData($sXpath, $aArrayToFeed)
276    {
277
278      $oDataFromXML = $this->templateEditor->default_data->xpath('//'.$sXpath);
279      $oDataFromXML = end($oDataFromXML);
280
281      foreach( $oDataFromXML as $sKey => $oData){
282
283        if (!empty($sKey)){
284
285          $sData = (string) $oData;
286
287          if ($oData->attributes()->twig == "on") {
288            $sData = Yii::app()->twigRenderer->convertTwigToHtml($sData);
289          }
290
291          $aArrayToFeed[$sKey] = $sData;
292        }
293      }
294
295      return $aArrayToFeed;
296    }
297
298    /**
299     * Returns all the twig strings inside the current XML. Used from TwigCommand
300     * NOTE: this not recursive. So it will show only the string of the current XML, not of parent XML. (not needed to generate twig cache from command line since all XML files are parsed)
301     *
302     * @param array $items if you already have a list of items and want to use it.
303     * @return array the list of strings using twig
304     */
305    public function getTwigStrings($items = array())
306    {
307      $oDataFromXML = $this->config;
308      $oElements = $oDataFromXML->xpath('//*[@twig="on"]');
309
310      foreach($oElements as $key => $oELement){
311        $items[] = (string) $oELement;
312      }
313
314      return $items;
315    }
316
317    /**
318     * Hard coded data for theme rendering outside of the normal survey taking.
319     *
320     * Currently used in two cases: theme editor preview, and twig cache file generation from command line.
321     */
322    public function getDefaultCoreDataForRendering()
323    {
324
325        $thissurvey = array();
326
327        // Values that never change.
328        $thissurvey['active'] = 'N';
329        $thissurvey['allowsave'] = "Y";
330        $thissurvey['active'] = "Y";
331        $thissurvey['tokenanswerspersistence'] = "Y";
332        $thissurvey['format'] = "G";
333
334        $thissurvey['usecaptcha'] = "A";
335        $thissurvey['showprogress'] = true;
336        $thissurvey['aNavigator']['show'] = true;
337        $thissurvey['aNavigator']['aMoveNext']['show'] = true;
338        $thissurvey['aNavigator']['aMovePrev']['show'] = true;
339
340        $thissurvey['alanguageChanger']['show'] = true;
341        $thissurvey['alanguageChanger']['datas'] = [
342            'sSelected' => 'en',
343            //'withForm' => true,  // Set to true for no-js functionality.
344            'aListLang' => [
345                'en' => gT('English'),
346                'de' => gT('German')
347            ]
348        ];
349
350
351        $thissurvey['aQuestionIndex']['bShow'] = true;
352        $thissurvey['aQuestionIndex']['items'] = [
353            [
354                'text' => gT('A group without step status styling')
355            ],
356            [
357                'text' => gT('This group is unanswered'),
358                'stepStatus' => [
359                    'index-item-unanswered' => true
360                ]
361            ],
362            [
363                'text' => gT('This group has an error'),
364                'stepStatus' => [
365                    'index-item-error' => true
366                ]
367            ],
368            [
369                'text' => gT('Current group is disabled'),
370                'stepStatus' => [
371                    'index-item-current' => true
372                ]
373            ]
374        ];
375
376        // Show "Clear all".
377        $thissurvey['bShowClearAll'] = true;
378
379        // Show language changer.
380        $thissurvey['alanguageChanger']['show'] = true;
381        $thissurvey['alanguageChanger']['datas'] = [
382            'sSelected' => 'en',
383            'aListLang' => [
384                'en' => gT('English'),
385                'de' => gT('German')
386            ]
387        ];
388
389        $thissurvey['aNavigator']['load'] = [
390            'show' => "Y"
391        ];
392
393
394        $thissurvey['aGroups'][1]["showdescription"] = true;
395        $thissurvey['aGroups'][1]["aQuestions"][1]["qid"]           = "1";
396        $thissurvey['aGroups'][1]["aQuestions"][1]["mandatory"]     = true;
397
398        // If called from command line to generate Twig temp, renderPartial doesn't exist in ConsoleApplication
399        if (method_exists ( Yii::app()->getController() , 'renderPartial' ) ){
400          $thissurvey['aGroups'][1]["aQuestions"][1]["answer"]        = Yii::app()->getController()->renderPartial('/admin/themes/templateeditor_question_answer_view', array(), true);
401        }
402        $thissurvey['aGroups'][1]["aQuestions"][1]["help"]["show"]  = true;
403        $thissurvey['aGroups'][1]["aQuestions"][1]["help"]["text"]  = gT("This is some helpful text.");
404        $thissurvey['aGroups'][1]["aQuestions"][1]["class"]         = "list-radio mandatory";
405        $thissurvey['aGroups'][1]["aQuestions"][1]["attributes"]    = 'id="question42"';
406
407        $thissurvey['aGroups'][1]["aQuestions"][2]["qid"]           = "1";
408        $thissurvey['aGroups'][1]["aQuestions"][2]["mandatory"]     = false;
409        if (method_exists ( Yii::app()->getController() , 'renderPartial' ) ){
410          $thissurvey['aGroups'][1]["aQuestions"][2]["answer"]        = Yii::app()->getController()->renderPartial('/admin/themes/templateeditor_question_answer_view', array('alt' => true), true);
411        }
412        $thissurvey['aGroups'][1]["aQuestions"][2]["help"]["show"]  = true;
413        $thissurvey['aGroups'][1]["aQuestions"][2]["help"]["text"]  = gT("This is some helpful text.");
414        $thissurvey['aGroups'][1]["aQuestions"][2]["class"]         = "text-long";
415        $thissurvey['aGroups'][1]["aQuestions"][2]["attributes"]    = 'id="question43"';
416
417        $thissurvey['aGroups'][1]["aQuestions"][1]['templateeditor'] = true;
418        $thissurvey['aGroups'][1]["aQuestions"][2]['templateeditor'] = true;
419
420        $thissurvey['registration_view'] = 'register_form';
421
422        $thissurvey['aCompleted']['showDefault'] = true;
423        $thissurvey['aCompleted']['aPrintAnswers']['show'] = true;
424        $thissurvey['aCompleted']['aPublicStatistics']['show'] = true;
425
426        $thissurvey['aAssessments']['show'] = true;
427
428
429        $thissurvey['aError']['title'] = gT("Error");
430        $thissurvey['aError']['message'] = gT("This is an error message example");
431
432        // Datas for assessments
433        $thissurvey['aAssessments']["datas"]["total"][0]["name"]       = gT("Welcome to the Assessment");
434        $thissurvey['aAssessments']["datas"]["total"][0]["min"]        = "0";
435        $thissurvey['aAssessments']["datas"]["total"][0]["max"]        = "3";
436        $thissurvey['aAssessments']["datas"]["total"][0]["message"]    = gT("You got {TOTAL} points out of 3 possible points.");
437        $thissurvey['aAssessments']["datas"]["total"]["show"]          = true;
438        $thissurvey['aAssessments']["datas"]["subtotal"]["show"]       = true;
439        $thissurvey['aAssessments']["datas"]["subtotal"]["datas"][2]   = 3;
440        $thissurvey['aAssessments']["datas"]["subtotal_score"][1]      = 3;
441        $thissurvey['aAssessments']["datas"]["total_score"]            = 3;
442
443        // Those values can be overwritten by XML
444        $thissurvey['name'] = gT("Template Sample");
445        $thissurvey['description'] =
446        "<p>".gT('This is a sample survey description. It could be quite long.')."</p>".
447        "<p>".gT("But this one isn't.")."<p>";
448        $thissurvey['welcome'] =
449        "<p>".gT('Welcome to this sample survey')."<p>".
450        "<p>".gT('You should have a great time doing this')."<p>";
451        $thissurvey['therearexquestions'] = gT('There is 1 question in this survey');
452        $thissurvey['surveyls_url'] = "https://www.limesurvey.org/";
453        $thissurvey['surveyls_urldescription'] = gT("Some URL description");
454
455        return $thissurvey;
456    }
457
458    /**
459     * Returns the layout file name for a given screen
460     *
461     * @param   string  $sScreen    the screen you want to retreive the files from. If null: all screens
462     * @return  string  the file name
463     */
464    public function getLayoutForScreen($sScreen)
465    {
466        if (empty($this->templateEditor)) {
467            return false;
468        }
469
470        $filesFromXML = $this->templateEditor->screens->xpath('//'.$sScreen.'/file');
471
472
473        foreach ($filesFromXML as $file) {
474
475            if ($file->attributes()->role == "layout") {
476                return (string) $file;
477            }
478        }
479
480        return false;
481    }
482
483
484
485    /**
486     * Returns the content file name for a given screen
487     *
488     * @param   string  $sScreen    the screen you want to retreive the files from. If null: all screens
489     * @return  string  the file name
490     */
491    public function getContentForScreen($sScreen)
492    {
493        if (empty($this->templateEditor)) {
494            return false;
495        }
496
497        $filesFromXML = $this->templateEditor->screens->xpath('//'.$sScreen.'/file');
498
499        foreach ($filesFromXML as $file) {
500
501            if ($file->attributes()->role == "content") {
502
503                // The path of the file is defined inside the theme itself.
504                $aExplodedFile = explode(DIRECTORY_SEPARATOR, $file);
505                $sFormatedFile = end($aExplodedFile);
506
507                // The file extension (.twig) is defined inside the theme itself.
508                $aExplodedFile = explode('.', $sFormatedFile);
509                $sFormatedFile = $aExplodedFile[0];
510                return (string) $sFormatedFile;
511            }
512        }
513
514        return false;
515    }
516
517    /**
518     * Retreives the absolute path for a file to edit (current template, mother template, etc)
519     * Also perform few checks (permission to edit? etc)
520     *
521     * @param string $sFile relative path to the file to edit
522     */
523    public function getFilePathForEditing($sFile, $aAllowedFiles = null)
524    {
525
526        // Check if the file is allowed for edition ($aAllowedFiles is produced via getValidScreenFiles() )
527        if (is_array($aAllowedFiles)) {
528            if (!in_array($sFile, $aAllowedFiles)) {
529                return false;
530            }
531        }
532
533        return $this->getFilePath($sFile, $this);
534    }
535
536    /**
537     * Copy a file from mother template to local directory and edit manifest if needed
538     *
539     * @return string template url
540     */
541    public function extendsFile($sFile)
542    {
543        if (!file_exists($this->path.$sFile) && !file_exists($this->viewPath.$sFile)) {
544
545            // Copy file from mother template to local directory
546            $sSourceFilePath = $this->getFilePath($sFile, $this);
547            $sDestinationFilePath = (pathinfo($sFile, PATHINFO_EXTENSION) == 'twig') ? $this->viewPath.$sFile : $this->path.$sFile;
548
549            //PHP 7 seems not to create the folder on copy automatically.
550            @mkdir(dirname($sDestinationFilePath), 0775, true);
551
552            copy($sSourceFilePath, $sDestinationFilePath);
553
554            // If it's a css or js file from config... must update DB and XML too....
555            $sExt = pathinfo($sDestinationFilePath, PATHINFO_EXTENSION);
556            if ($sExt == "css" || $sExt == "js") {
557
558                // Check if that CSS/JS file is in DB/XML
559                $aFiles = $this->getFilesForPackages($sExt, $this);
560                $sFile  = str_replace('./', '', $sFile);
561
562                // The CSS/JS file is a configuration one....
563                if (in_array($sFile, $aFiles)) {
564                    $this->addFileReplacement($sFile, $sExt);
565                    $this->addFileReplacementInDB($sFile, $sExt);
566                }
567            }
568        }
569        return $this->getFilePath($sFile, $this);
570    }
571
572    /**
573     * Get the files (css or js) defined in the manifest of a template and its mother templates
574     *
575     * @param  string $type       css|js
576     * @param string $oRTemplate template from which the recurrence should start
577     * @return array
578     */
579    public function getFilesForPackages($type, $oRTemplate)
580    {
581        $aFiles = array();
582        while (is_a($oRTemplate, 'TemplateManifest')) {
583            $aTFiles = isset($oRTemplate->config->files->$type->filename) ? (array) $oRTemplate->config->files->$type->filename : array();
584            $aFiles  = array_merge($aTFiles, $aFiles);
585            $oRTemplate = $oRTemplate->oMotherTemplate;
586        }
587        return $aFiles;
588    }
589
590    /**
591     * Add a file replacement entry in DB
592     * In the first place it tries to get the all the configuration entries for this template
593     * (it can be void if edited from template editor, or they can be numerous if the template has local config at survey/survey group/user level)
594     * Then, it call $oTemplateConfiguration->addFileReplacement($sFile, $sType) for each one of them.
595     *
596     * @param string $sFile the file to replace
597     * @param string $sType css|js
598     */
599    public function addFileReplacementInDB($sFile, $sType)
600    {
601        $oTemplateConfigurationModels = TemplateConfiguration::model()->findAllByAttributes(array('template_name'=>$this->sTemplateName));
602        foreach ($oTemplateConfigurationModels as $oTemplateConfigurationModel) {
603            $oTemplateConfigurationModel->addFileReplacement($sFile, $sType);
604        }
605    }
606
607    /**
608     * Get the list of all the files inside the file folder for a template and its mother templates
609     * @return array
610     */
611    public function getOtherFiles()
612    {
613        $otherfiles = array();
614
615        if (!empty($this->oMotherTemplate)) {
616            $otherfiles = $this->oMotherTemplate->getOtherFiles();
617        }
618
619        if (file_exists($this->filesPath) && $handle = opendir($this->filesPath)) {
620
621            while (false !== ($file = readdir($handle))) {
622                if (!array_search($file, array("DUMMYENTRY", ".", "..", "preview.png"))) {
623                    if (!is_dir($this->viewPath.DIRECTORY_SEPARATOR.$file)) {
624                        $otherfiles[$file] = $this->filesPath.DIRECTORY_SEPARATOR.$file;
625                    }
626                }
627            }
628
629            closedir($handle);
630        }
631        return $otherfiles;
632    }
633
634
635    /**
636     *
637     */
638    public function getTemplateURL()
639    {
640
641      // By default, theme folder is always the folder name. @See:TemplateConfig::importManifest().
642      if (Template::isStandardTemplate($this->sTemplateName)) {
643          return Yii::app()->getConfig("standardthemerooturl").'/'.$this->sTemplateName.'/';
644      } else {
645          return  Yii::app()->getConfig("userthemerooturl").'/'.$this->sTemplateName.'/';
646      }
647
648    //    return Template::getTemplateURL($this->sTemplateName);
649    }
650
651
652    public function getButtons()
653    {
654        $sEditorUrl  = Yii::app()->getController()->createUrl('admin/themes/sa/view', array("templatename"=>$this->sTemplateName));
655        $sDeleteUrl   = Yii::app()->getController()->createUrl('admin/themes/sa/deleteAvailableTheme/');
656
657
658        // TODO: load to DB
659        $sEditorLink = "<a
660            id='template_editor_link_".$this->sTemplateName."'
661            href='".$sEditorUrl."'
662            class='btn btn-default btn-block'>
663                <span class='icon-templates'></span>
664                ".gT('Theme editor')."
665            </a>";
666
667            //
668
669        $sLoadLink = CHtml::form( array("/admin/themeoptions/sa/importmanifest/"), 'post',array('id'=>'frmínstalltheme','name'=>'frmínstalltheme')) .
670                "<input type='hidden' name='templatename' value='".$this->sTemplateName."'>
671                <button id='template_options_link_".$this->sTemplateName."'
672                class='btn btn-default btn-block'>
673                    <span class='fa fa-download text-warning'></span>
674                    ".gT('Install')."
675                </button>
676                </form>";
677
678
679        $sDeleteLink = '';
680        // We don't want user to be able to delete standard theme. Must be done via ftp (advanced users only)
681        if(Permission::model()->hasGlobalPermission('templates','delete') && !Template::isStandardTemplate($this->sTemplateName) ){
682          $sDeleteLink = '<a
683              id="template_delete_link_'.$this->sTemplateName.'"
684              href="'.$sDeleteUrl.'"
685              data-post=\'{ "templatename": "'.$this->sTemplateName.'" }\'
686              data-text="'.gT('Are you sure you want to delete this theme? ').'"
687              title="'.gT('Delete').'"
688              class="btn btn-danger btn-block selector--ConfirmModal">
689                  <span class="fa fa-trash "></span>
690                  '.gT('Delete').'
691                  </a>';
692      }
693
694      return $sEditorLink.$sLoadLink.$sDeleteLink;
695
696    }
697
698    /**
699     * Create a new entry in {{templates}} and {{template_configuration}} table using the template manifest
700     * @param string $sTemplateName the name of the template to import
701     * @return boolean true on success | exception
702     * @throws Exception
703     */
704    public static function importManifest($sTemplateName, $aDatas = array())
705    {
706        $oTemplate                  = Template::getTemplateConfiguration($sTemplateName, null, null, true);
707        $aDatas['extends']          = $bExtends = (string) $oTemplate->config->metadata->extends;
708
709        if ($bExtends && !Template::model()->findByPk($bExtends)) {
710            Yii::app()->setFlashMessage(sprintf(gT("You can't import the theme '%s' because '%s'  is not installed."), $sTemplateName, $bExtends), 'error');
711            Yii::app()->getController()->redirect(array("admin/themeoptions"));
712        }
713
714        // Metadas is never inherited
715        $aDatas['api_version']      = (string) $oTemplate->config->metadata->apiVersion;
716        $aDatas['author_email']     = (string) $oTemplate->config->metadata->authorEmail;
717        $aDatas['author_url']       = (string) $oTemplate->config->metadata->authorUrl;
718        $aDatas['copyright']        = (string) $oTemplate->config->metadata->copyright;
719        $aDatas['version']          = (string) $oTemplate->config->metadata->version;
720        $aDatas['license']          = (string) $oTemplate->config->metadata->license;
721        $aDatas['description']      = (string) $oTemplate->config->metadata->description;
722
723        // Engine, files, and options can be inherited from a moter template
724        // It means that the while field should always be inherited, not a subfield (eg: all files, not only css add)
725        $oREngineTemplate = (!empty($bExtends)) ? self::getTemplateForXPath($oTemplate, 'engine') : $oTemplate;
726
727
728        $aDatas['view_folder']       = (string) $oREngineTemplate->config->engine->viewdirectory;
729        $aDatas['files_folder']      = (string) $oREngineTemplate->config->engine->filesdirectory;
730        $aDatas['cssframework_name'] = (string) $oREngineTemplate->config->engine->cssframework->name;
731        $aDatas['cssframework_css']  = self::getAssetsToReplaceFormated($oREngineTemplate->config->engine, 'css'); //self::formatArrayFields($oREngineTemplate, 'engine', 'cssframework_css');
732        $aDatas['cssframework_js']   = self::formatArrayFields($oREngineTemplate, 'engine', 'cssframework_js');
733        $aDatas['packages_to_load']  = self::formatArrayFields($oREngineTemplate, 'engine', 'packages');
734
735
736        // If empty in manifest, it should be the field in db, so the Mother Template css/js files will be used...
737        if (is_object($oTemplate->config->files)) {
738            $aDatas['files_css']         = self::formatArrayFields($oTemplate, 'files', 'css');
739            $aDatas['files_js']          = self::formatArrayFields($oTemplate, 'files', 'js');
740            $aDatas['files_print_css']   = self::formatArrayFields($oTemplate, 'files', 'print_css');
741        } else {
742            $aDatas['files_css'] = $aDatas['files_js'] = $aDatas['files_print_css'] = null;
743        }
744
745        $aDatas['aOptions'] = (!empty($oTemplate->config->options[0]) && count($oTemplate->config->options[0]) == 0) ? array() : $oTemplate->config->options[0]; // If template provide empty options, it must be cleaned to avoid crashes
746
747        return parent::importManifest($sTemplateName, $aDatas);
748    }
749
750    /**
751     * Create a new entry in {{template_configuration}} table using the survey theme options from lss export file
752     * @param     $iSurveyId      int    the id of the survey
753     * @param $xml SimpleXMLElement
754     * @return boolean true on success
755     */
756    public static function importManifestLss($iSurveyId = 0, $xml =null)
757    {
758        if ((int)$iSurveyId > 0 && !empty($xml)){
759            $oTemplateConfiguration = new TemplateConfiguration;
760            $oTemplateConfiguration->setToInherit();
761
762            $oTemplateConfiguration->bJustCreated = true;
763            $oTemplateConfiguration->isNewRecord = true;
764            $oTemplateConfiguration->id = null;
765            $oTemplateConfiguration->template_name = $xml->template_name->__toString();
766            $oTemplateConfiguration->sid = $iSurveyId;
767
768            if (isAssociativeArray((array)$xml->config->options)){
769                $oTemplateConfiguration->options  = TemplateConfig::formatToJsonArray($xml->config->options);
770            }
771
772            if ($oTemplateConfiguration->save()){
773                return true;
774            }
775        }
776
777        return false;
778    }
779
780    /**
781     * @param string $sFieldPath
782     */
783    public static function getTemplateForXPath($oTemplate, $sFieldPath)
784    {
785        $oRTemplate = $oTemplate;
786        while (!is_object($oRTemplate->config->$sFieldPath) || empty($oRTemplate->config->$sFieldPath)) {
787            $sRTemplateName = (string) $oRTemplate->config->metadata->extends;
788
789            if (!empty($sRTemplateName)) {
790                $oRTemplate = Template::getTemplateConfiguration($sRTemplateName, null, null, true);
791                if (!is_a($oRTemplate, 'TemplateManifest')) {
792                    // Think about what to do..
793                    throw new Exception("Error: Can't find a template for '$oRTemplate->sTemplateName' in xpath '$sFieldPath'.");
794                }
795            } else {
796                throw new Exception("Error: Can't find a template for '$oRTemplate->sTemplateName' in xpath '$sFieldPath'.");
797            }
798        }
799
800        return $oRTemplate;
801    }
802
803    /**
804     * This will prepare an array for the field, so the json_encode will create
805     * If a field is empty, its value should not be null, but an empty array for the json encoding in DB
806     *
807     * @param TemplateManifest $oTemplate
808     * @param string $sFieldPath path to the field (under config)
809     * @param string $sFieldName name of the field
810     * @return array field value | empty array
811     */
812    public static function formatArrayFields($oTemplate, $sFieldPath, $sFieldName)
813    {
814        return (empty($oTemplate->config->$sFieldPath->$sFieldName->value) && empty($oTemplate->config->$sFieldPath->$sFieldName)) ? array() : $oTemplate->config->$sFieldPath->$sFieldName;
815    }
816
817    /**
818     * Get the DOMDocument of the Manifest
819     * @param  string      $sConfigPath path where to find the manifest
820     * @return DOMDocument
821     */
822    public static function getManifestDOM($sConfigPath)
823    {
824        // First we get the XML file
825        $oNewManifest = new DOMDocument();
826        $oNewManifest->load($sConfigPath."/config.xml");
827        return $oNewManifest;
828    }
829
830
831    /**
832     * Change the name inside the DOMDocument (will not save it)
833     * @param DOMDocument   $oNewManifest  The DOMDOcument of the manifest
834     * @param string        $sName         The wanted name
835     */
836    public static function changeNameInDOM($oNewManifest, $sName)
837    {
838        $oConfig      = $oNewManifest->getElementsByTagName('config')->item(0);
839        $ometadata = $oConfig->getElementsByTagName('metadata')->item(0);
840        $oOldNameNode = $ometadata->getElementsByTagName('name')->item(0);
841        $oNvNameNode  = $oNewManifest->createElement('name', $sName);
842        $ometadata->replaceChild($oNvNameNode, $oOldNameNode);
843    }
844
845    /**
846     * Change the date inside the DOMDocument
847     * @param DOMDocument   $oNewManifest  The DOMDOcument of the manifest
848     * @param string        $sDate         The wanted date, if empty the current date with config time adjustment will be used
849     */
850    public static function changeDateInDOM($oNewManifest, $sDate = '')
851    {
852        $sDate = empty($sDate) ? dateShift(date("Y-m-d H:i:s"), "Y-m-d H:i", Yii::app()->getConfig("timeadjust")) : $sDate;
853        $oConfig = $oNewManifest->getElementsByTagName('config')->item(0);
854        $ometadata = $oConfig->getElementsByTagName('metadata')->item(0);
855        if($ometadata->getElementsByTagName('creationDate')) {
856            $oOldDateNode   = $ometadata->getElementsByTagName('creationDate')->item(0);
857        }
858        $oNvDateNode    = $oNewManifest->createElement('creationDate', $sDate);
859        if(empty($oOldDateNode)) {
860            $ometadata->appendChild($oNvDateNode);
861        } else {
862            $ometadata->replaceChild($oNvDateNode, $oOldDateNode);
863        }
864        if($ometadata->getElementsByTagName('last_update')) {
865            $oOldUpdateNode   = $ometadata->getElementsByTagName('last_update')->item(0);
866        }
867        $oNvDateNode    = $oNewManifest->createElement('last_update', $sDate);
868        if(empty($oOldUpdateNode)) {
869            $ometadata->appendChild($oNvDateNode);
870        } else {
871            $ometadata->replaceChild($oNvDateNode, $oOldUpdateNode);
872        }
873    }
874
875    /**
876     * Change the template name inside the manifest (called from template editor)
877     * NOTE: all tests (like template exist, etc) are done from template controller.
878     *
879     * @param string $sOldName The old name of the template
880     * @param string $sNewName The newname of the template
881     */
882    public static function rename($sOldName, $sNewName)
883    {
884        libxml_disable_entity_loader(false);
885        $sConfigPath = Yii::app()->getConfig('userthemerootdir')."/".$sNewName;
886        $oNewManifest = self::getManifestDOM($sConfigPath);
887        self::changeNameInDOM($oNewManifest, $sNewName);
888        self::changeDateInDOM($oNewManifest);
889        $oNewManifest->save($sConfigPath."/config.xml");
890        libxml_disable_entity_loader(true);
891    }
892
893    /**
894     * Delete files and engine node inside the DOM
895     *
896     * @param DOMDocument   $oNewManifest  The DOMDOcument of the manifest
897     */
898    public static function deleteEngineInDom($oNewManifest)
899    {
900        $oConfig            = $oNewManifest->getElementsByTagName('config')->item(0);
901
902        // Then we delete the nodes that should be inherit
903        $aNodesToDelete     = array();
904        //$aNodesToDelete[]   = $oConfig->getElementsByTagName('files')->item(0);
905        $aNodesToDelete[]   = $oConfig->getElementsByTagName('engine')->item(0);
906
907        foreach ($aNodesToDelete as $node) {
908            // If extended template already extend another template, it will not have those nodes
909            if (is_a($node, 'DOMNode')) {
910                $oConfig->removeChild($node);
911            }
912        }
913    }
914
915    /**
916     * Change author inside the DOM
917     *
918     * @param DOMDocument   $oNewManifest  The DOMDOcument of the manifest
919     */
920    public static function changeAuthorInDom($oNewManifest)
921    {
922        $oConfig          = $oNewManifest->getElementsByTagName('config')->item(0);
923        $ometadata = $oConfig->getElementsByTagName('metadata')->item(0);
924        $oOldAuthorNode   = $ometadata->getElementsByTagName('author')->item(0);
925        $oNvAuthorNode    = $oNewManifest->createElement('author', Yii::app()->user->name);
926        $ometadata->replaceChild($oNvAuthorNode, $oOldAuthorNode);
927    }
928
929    /**
930     * Change author email inside the DOM
931     *
932     * @param DOMDocument   $oNewManifest  The DOMDOcument of the manifest
933     */
934    public static function changeEmailInDom($oNewManifest)
935    {
936        $oConfig        = $oNewManifest->getElementsByTagName('config')->item(0);
937        $ometadata = $oConfig->getElementsByTagName('metadata')->item(0);
938        $oOldMailNode   = $ometadata->getElementsByTagName('authorEmail')->item(0);
939        $oNvMailNode    = $oNewManifest->createElement('authorEmail', htmlspecialchars(Yii::app()->getConfig('siteadminemail')));
940        $ometadata->replaceChild($oNvMailNode, $oOldMailNode);
941    }
942
943    /**
944     * Change the extends node inside the DOM
945     * If it doesn't exist, it will create it
946     * @param DOMDocument   $oNewManifest  The DOMDOcument of the manifest
947     * @param string        $sToExtends    Name of the template to extends
948     */
949    public static function changeExtendsInDom($oNewManifest, $sToExtends)
950    {
951        $oExtendsNode = $oNewManifest->createElement('extends', $sToExtends);
952        $oConfig = $oNewManifest->getElementsByTagName('config')->item(0);
953        $ometadata = $oConfig->getElementsByTagName('metadata')->item(0);
954
955        // We test if mother template already extends another template
956        if (!empty($ometadata->getElementsByTagName('extends')->item(0))) {
957            $ometadata->replaceChild($oExtendsNode, $ometadata->getElementsByTagName('extends')->item(0));
958        } else {
959            $ometadata->appendChild($oExtendsNode);
960        }
961    }
962
963
964    /**
965     * Update the config file of a given template so that it extends another one
966     *
967     * It will:
968     * 1. Delete files and engine nodes
969     * 2. Update the name of the template
970     * 3. Change the creation/modification date to the current date
971     * 4. Change the autor name to the current logged in user
972     * 5. Change the author email to the admin email
973     *
974     * Used in template editor
975     * Both templates and configuration files must exist before using this function
976     *
977     * It's used when extending a template from template editor
978     * @param   string  $sToExtends     the name of the template to extend
979     * @param   string  $sNewName       the name of the new template
980     */
981    public static function extendsConfig($sToExtends, $sNewName)
982    {
983        $sConfigPath = Yii::app()->getConfig('userthemerootdir')."/".$sNewName;
984
985        // First we get the XML file
986        $oldState = libxml_disable_entity_loader(false);
987        $oNewManifest = self::getManifestDOM($sConfigPath);
988
989        self::deleteEngineInDom($oNewManifest);
990        self::changeNameInDOM($oNewManifest, $sNewName);
991        self::changeDateInDOM($oNewManifest);
992        self::changeAuthorInDom($oNewManifest);
993        self::changeEmailInDom($oNewManifest);
994        self::changeExtendsInDom($oNewManifest, $sToExtends);
995
996        $oNewManifest->save($sConfigPath."/config.xml");
997
998        libxml_disable_entity_loader($oldState);
999    }
1000
1001    /**
1002     * Read the config.xml file of the template and push its contents to $this->config
1003     */
1004    private function readManifest()
1005    {
1006        $this->xmlFile = $this->path.'config.xml';
1007
1008        if (file_exists(realpath($this->xmlFile))) {
1009            $bOldEntityLoaderState = libxml_disable_entity_loader(true); // @see: http://phpsecurity.readthedocs.io/en/latest/Injection-Attacks.html#xml-external-entity-injection
1010            $sXMLConfigFile        = file_get_contents(realpath($this->xmlFile)); // @see: Now that entity loader is disabled, we can't use simplexml_load_file; so we must read the file with file_get_contents and convert it as a string
1011            $oDOMConfig = new DOMDocument;
1012            $oDOMConfig->loadXML($sXMLConfigFile);
1013            $oXPath = new DOMXpath($oDOMConfig);
1014            foreach ($oXPath->query('//comment()') as $oComment) {
1015                $oComment->parentNode->removeChild($oComment);
1016            }
1017            $oXMLConfig = simplexml_import_dom($oDOMConfig);
1018            foreach ($oXMLConfig->config->xpath("//file") as $oFileName) {
1019                        $oFileName[0] = get_absolute_path($oFileName[0]);
1020            }
1021
1022            $this->config = $oXMLConfig; // Using PHP >= 5.4 then no need to decode encode + need attributes : then other function if needed :https://secure.php.net/manual/en/book.simplexml.php#108688 for example
1023            libxml_disable_entity_loader($bOldEntityLoaderState); // Put back entity loader to its original state, to avoid contagion to other applications on the server
1024        } else {
1025            throw new Exception(" Error: Can't find a manifest for $this->sTemplateName in ' $this->path ' ");
1026        }
1027    }
1028
1029    /**
1030     * Set the path of the current template
1031     * It checks if it's a core or a user template, if it exists, and if it has a config file
1032     */
1033    private function setPath()
1034    {
1035        // If the template is standard, its root is based on standardthemerootdir, else, it is a user template, its root is based on userthemerootdir
1036        $this->path = ($this->isStandard) ? Yii::app()->getConfig("standardthemerootdir").DIRECTORY_SEPARATOR.$this->sTemplateName.DIRECTORY_SEPARATOR : Yii::app()->getConfig("userthemerootdir").DIRECTORY_SEPARATOR.$this->sTemplateName.DIRECTORY_SEPARATOR;
1037
1038        // If the template directory doesn't exist, we just set Default as the template to use
1039        // TODO: create a method "setToDefault"
1040        if (!is_dir($this->path)) {
1041            if (!$this->iSurveyId) {
1042                \SettingGlobal::setSetting('defaulttheme',Yii::app()->getConfig('defaultfixedtheme'));
1043                /* @todo ? : check if installed, install if not */
1044            }
1045            $this->sTemplateName = Yii::app()->getConfig('defaulttheme');
1046            if(Template::isStandardTemplate(Yii::app()->getConfig('defaulttheme'))) {
1047                $this->isStandard    = true;
1048                $this->path = Yii::app()->getConfig("standardthemerootdir").DIRECTORY_SEPARATOR.$this->sTemplateName.DIRECTORY_SEPARATOR;
1049            } else {
1050                $this->isStandard    = false;
1051                $this->path = Yii::app()->getConfig("userthemerootdir").DIRECTORY_SEPARATOR.$this->sTemplateName.DIRECTORY_SEPARATOR;
1052            }
1053        }
1054
1055        // If the template doesn't have a config file (maybe it has been deleted, or whatever),
1056        // then, we load the default template
1057        $this->hasConfigFile = (string) is_file($this->path.'config.xml');
1058        if (!$this->hasConfigFile) {
1059            $this->path = Yii::app()->getConfig("standardthemerootdir").DIRECTORY_SEPARATOR.$this->sTemplateName.DIRECTORY_SEPARATOR;
1060
1061        }
1062    }
1063
1064    /**
1065     * Set the template name.
1066     * If no templateName provided, then a survey id should be given (it will then load the template related to the survey)
1067     *
1068     * @var     $sTemplateName  string the name of the template
1069     * @var     $iSurveyId      int    the id of the survey
1070     */
1071    private function setTemplateName($sTemplateName = '', $iSurveyId = '')
1072    {
1073        // If it is called from the template editor, a template name will be provided.
1074        // If it is called for survey taking, a survey id will be provided
1075        if ($sTemplateName == '' && $iSurveyId == '') {
1076            /* Some controller didn't test completely survey id (PrintAnswersController for example), then set to default here */
1077            $sTemplateName = App()->getConfig('defaulttheme');
1078        }
1079
1080        $this->sTemplateName = $sTemplateName;
1081        $this->iSurveyId     = (int) $iSurveyId;
1082
1083        if ($sTemplateName == '') {
1084            $oSurvey = Survey::model()->findByPk($iSurveyId);
1085
1086            if ($oSurvey) {
1087                $this->sTemplateName = $oSurvey->template;
1088            } else {
1089                $this->sTemplateName = App()->getConfig('defaulttheme');
1090            }
1091        }
1092    }
1093
1094
1095    /**
1096     * Specific Integration of TemplateConfig.
1097     */
1098
1099
1100    public function setBasics($sTemplateName = '', $iSurveyId = '', $bUseMagicInherit = false)
1101    {
1102        // In manifest mode, we always use the default value from manifest, so no inheritance, no $bUseMagicInherit set needed
1103        $this->setTemplateName($sTemplateName, $iSurveyId); // Check and set template name
1104        $this->setIsStandard(); // Check if  it is a CORE template
1105        $this->setPath(); // Check and set path
1106        $this->readManifest(); // Check and read the manifest to set local params
1107    }
1108
1109    /**
1110     * Get showpopups value from config or template configuration
1111     */
1112    public function getshowpopups(){
1113        $config = (int)Yii::app()->getConfig('showpopups');
1114        if ($config == 2){
1115            if (isset($this->oOptions->showpopups)){
1116                $this->showpopups = (int)$this->oOptions->showpopups;
1117            } else {
1118               $this->showpopups = 1;
1119           }
1120        } else {
1121            $this->showpopups = $config;
1122        }
1123    }
1124
1125    /**
1126     * Add a file replacement entry
1127     * eg: <filename replace="css/template.css">css/template.css</filename>
1128     *
1129     * @param string $sFile the file to replace
1130     * @param string $sType css|js
1131     */
1132    public function addFileReplacement($sFile, $sType)
1133    {
1134        // First we get the XML file
1135        libxml_disable_entity_loader(false);
1136        $oNewManifest = new DOMDocument();
1137        $oNewManifest->load($this->path."config.xml");
1138
1139        $oConfig   = $oNewManifest->getElementsByTagName('config')->item(0);
1140        $oFiles    = $oNewManifest->getElementsByTagName('files')->item(0);
1141        $oOptions  = $oNewManifest->getElementsByTagName('options')->item(0); // Only for the insert before statement
1142
1143        if (is_null($oFiles)) {
1144            $oFiles = $oNewManifest->createElement('files');
1145        }
1146
1147        $oAssetType = $oFiles->getElementsByTagName($sType)->item(0);
1148        if (is_null($oAssetType)) {
1149            $oAssetType = $oNewManifest->createElement($sType);
1150            $oFiles->appendChild($oAssetType);
1151        }
1152
1153        $oNewManifest->createElement('filename');
1154
1155        $oAssetElem       = $oNewManifest->createElement('filename', $sFile);
1156        $replaceAttribute = $oNewManifest->createAttribute('replace');
1157        $replaceAttribute->value = $sFile;
1158        $oAssetElem->appendChild($replaceAttribute);
1159        $oAssetType->appendChild($oAssetElem);
1160        $oConfig->insertBefore($oFiles, $oOptions);
1161        $oNewManifest->save($this->path."config.xml");
1162        libxml_disable_entity_loader(true);
1163    }
1164
1165    /**
1166     * From a list of json files in db it will generate a PHP array ready to use by removeFileFromPackage()
1167     *
1168     * @var $sType string js or css ?
1169     * @return array
1170     */
1171    protected function getFilesTo($oTemplate, $sType, $sAction)
1172    {
1173        $aFiles = array();
1174        $oRFilesTemplate = (!empty($bExtends)) ? self::getTemplateForXPath($oTemplate, 'files') : $oTemplate;
1175
1176        if (isset($oRFilesTemplate->config->files->$sType->$sAction)) {
1177            $aFiles = (array) $oTemplate->config->files->$sType->$sAction;
1178        }
1179
1180        return $aFiles;
1181    }
1182
1183
1184    /**
1185     * Proxy for Yii::app()->clientScript->removeFileFromPackage()
1186     * It's not realy needed here, but it is needed for TemplateConfiguration model.
1187     * So, we use it here to have the same interface for TemplateManifest and TemplateConfiguration,
1188     * So, in the future, we'll can both inherit them from a same object (best would be to extend CModel to create a LSYii_Template)
1189     *
1190     * @param string $sPackageName     string   name of the package to edit
1191     * @param $sType            string   the type of settings to change (css or js)
1192     * @param $aSettings        array    array of local setting
1193     * @return array
1194     */
1195    protected function removeFileFromPackage($sPackageName, $sType, $aSetting)
1196    {
1197        Yii::app()->clientScript->removeFileFromPackage($sPackageName, $sType, $aSetting);
1198    }
1199
1200    /**
1201     * Configure the mother template (and its mother templates)
1202     * This is an object recursive call to TemplateManifest::prepareTemplateRendering()
1203     */
1204    protected function setMotherTemplates()
1205    {
1206        if (isset($this->config->metadata->extends)) {
1207            $sMotherTemplateName   = (string) $this->config->metadata->extends;
1208            if (!empty($sMotherTemplateName)){
1209
1210                $instance= Template::getTemplateConfiguration($sMotherTemplateName, null, null, true);
1211                $instance->prepareTemplateRendering($sMotherTemplateName);
1212                $this->oMotherTemplate = $instance; // $instance->prepareTemplateRendering($sMotherTemplateName, null);
1213            }
1214
1215        }
1216    }
1217
1218    /**
1219     * @param TemplateManifest $oRTemplate
1220     * @param string $sPath
1221     */
1222    protected function getTemplateForPath($oRTemplate, $sPath)
1223    {
1224        while (empty($oRTemplate->config->xpath($sPath))) {
1225            $oMotherTemplate = $oRTemplate->oMotherTemplate;
1226            if (!($oMotherTemplate instanceof TemplateConfiguration)) {
1227                throw new Exception("Error: Can't find a template for '$oRTemplate->sTemplateName' in xpath '$sPath'.");
1228            }
1229            $oRTemplate = $oMotherTemplate;
1230        }
1231        return $oRTemplate;
1232    }
1233
1234    /**
1235     * Set the default configuration values for the template, and use the motherTemplate value if needed
1236     */
1237    protected function setThisTemplate()
1238    {
1239        // Mandtory setting in config XML (can be not set in inheritance tree, but must be set in mother template (void value is still a setting))
1240        $this->apiVersion         = (isset($this->config->metadata->apiVersion)) ? $this->config->metadata->apiVersion : null;
1241
1242
1243        $this->viewPath           = $this->path.$this->getTemplateForPath($this, '//viewdirectory')->config->engine->viewdirectory.DIRECTORY_SEPARATOR;
1244        $this->filesPath          = $this->path.$this->getTemplateForPath($this, '//filesdirectory')->config->engine->filesdirectory.DIRECTORY_SEPARATOR;
1245        $this->templateEditor     = $this->getTemplateForPath($this, '//template_editor')->config->engine->template_editor;
1246
1247        // Options are optional
1248        if (!empty($this->config->xpath("//options"))) {
1249            $aOptions = $this->config->xpath("//options");
1250            $this->oOptions = $aOptions[0];
1251        } elseif (!empty($this->oMotherTemplate->oOptions)) {
1252            $this->oOptions = $this->oMotherTemplate->oOptions;
1253        } else {
1254            $this->oOptions = "";
1255        }
1256
1257        // Not mandatory (use package dependances)
1258        $this->cssFramework             = (!empty($this->config->xpath("//cssframework"))) ? $this->config->engine->cssframework : '';
1259        // Add depend package according to packages
1260        $this->depends                  = array_merge($this->depends, $this->getDependsPackages($this));
1261
1262        //Add extra packages from xml
1263        $this->packages                 = array();
1264        $packageActionFromEngineSection = json_decode(json_encode($this->config->engine->packages));
1265        if (!empty($packageActionFromEngineSection)) {
1266            if (!empty($packageActionFromEngineSection->add)) {
1267                $this->packages = array_merge(
1268                    !is_array($packageActionFromEngineSection->add) ? [$packageActionFromEngineSection->add] : $packageActionFromEngineSection->add,
1269                    $this->packages
1270                );
1271            }
1272            if (!empty($packageActionFromEngineSection->remove)) {
1273                $this->packages =  array_diff($this->packages, $packageActionFromEngineSection->remove);
1274            }
1275        }
1276        $this->depends = array_merge($this->depends, $this->packages);
1277    }
1278
1279
1280    protected function addMotherTemplatePackage($packages)
1281    {
1282        if (isset($this->config->metadata->extends)) {
1283            $sMotherTemplateName = (string) $this->config->metadata->extends;
1284            $packages[]          = 'survey-template-'.$sMotherTemplateName;
1285        }
1286        return $packages;
1287    }
1288
1289    /**
1290     * Get the list of file replacement from Engine Framework
1291     * @param string  $sType            css|js the type of file
1292     * @param boolean $bInlcudeRemove   also get the files to remove
1293     * @return array
1294     */
1295    protected function getFrameworkAssetsToReplace($sType, $bInlcudeRemove = false)
1296    {
1297        $aAssetsToRemove = array();
1298        if (!empty($this->cssFramework->$sType) && !empty($this->cssFramework->$sType->attributes()->replace)) {
1299            $aAssetsToRemove = (array) $this->cssFramework->$sType->attributes()->replace;
1300            if ($bInlcudeRemove) {
1301                $aAssetsToRemove = array_merge($aAssetsToRemove, (array) $this->cssFramework->$sType->attributes()->remove);
1302            }
1303        }
1304        return $aAssetsToRemove;
1305    }
1306
1307
1308
1309    /**
1310     * Get the list of file replacement from Engine Framework
1311     * @param string  $sType            css|js the type of file
1312     * @param boolean $bInlcudeRemove   also get the files to remove
1313     * @return stdClass
1314     */
1315    static public function getAssetsToReplaceFormated($oEngine, $sType, $bInlcudeRemove = false)
1316    {
1317        $oAssetsToReplaceFormated = new stdClass();
1318        if (!empty($oEngine->cssframework->$sType) && !empty($oEngine->cssframework->$sType->attributes()->replace)) {
1319            //var_dump($oEngine->cssframework->$sType);  die();
1320
1321            $sAssetsToReplace   = (string) $oEngine->cssframework->$sType->attributes()->replace;
1322            $sAssetsReplacement = (string) $oEngine->cssframework->$sType;
1323
1324            // {"replace":[["css/bootstrap.css","css/cerulean.css"]]}
1325            $oAssetsToReplaceFormated->replace = array(array($sAssetsToReplace, $sAssetsReplacement));
1326
1327        }
1328        return $oAssetsToReplaceFormated;
1329    }
1330
1331    /**
1332     * Get the list of file replacement from Engine Framework
1333     * @param string  $sType            css|js the type of file
1334     * @return array
1335     */
1336    protected function getFrameworkAssetsReplacement($sType)
1337    {
1338        $aAssetsToRemove = array();
1339        if (!empty($this->cssFramework->$sType)) {
1340            $nodes = (array) $this->config->xpath('//cssframework/'.$sType.'[@replace]');
1341            if (!empty($nodes)) {
1342                foreach ($nodes as $key => $node) {
1343                    $nodes[$key] = (string) $node[0];
1344                }
1345
1346                $aAssetsToRemove = $nodes;
1347            }
1348        }
1349        return $aAssetsToRemove;
1350    }
1351
1352    /**
1353     * @return string
1354     */
1355    public function getTemplateAndMotherNames()
1356    {
1357        $oRTemplate = $this;
1358        $sTemplateNames = $this->sTemplateName;
1359
1360        while (!empty($oRTemplate->oMotherTemplate)) {
1361
1362            $sTemplateNames .= ' ' . $oRTemplate->config->metadata->extends;
1363            $oRTemplate      = $oRTemplate->oMotherTemplate;
1364            if (!($oRTemplate instanceof TemplateConfiguration)) {
1365                // Throw alert: should not happen
1366                break;
1367            }
1368        }
1369
1370        return $sTemplateNames;
1371    }
1372
1373    /**
1374     * Twig statements can be used in Theme description
1375     * Override method from TemplateConfiguration to use the description from the XML
1376     * @return string description from the xml
1377     */
1378    public function getDescription()
1379    {
1380        $sDescription = $this->config->metadata->description;
1381
1382        // If wrong Twig in manifest, we don't want to block the whole list rendering
1383        // Note: if no twig statement in the description, twig will just render it as usual
1384        try {
1385            $sDescription = Yii::app()->twigRenderer->convertTwigToHtml($this->config->metadata->description);
1386        } catch (\Exception $e) {
1387            // It should never happen, but let's avoid to anoy final user in production mode :)
1388            if (YII_DEBUG) {
1389                Yii::app()->setFlashMessage(
1390                    "Twig error in template " .
1391                    $this->sTemplateName .
1392                    " description <br> Please fix it and reset the theme <br>" . $e->getMessage(),
1393                    'error'
1394                );
1395            }
1396        }
1397
1398        return $sDescription;
1399    }
1400
1401    /**
1402     * PHP getter magic method.
1403     * This method is overridden so that AR attributes can be accessed like properties.
1404     * @param string $name property name
1405     * @return mixed property value
1406     * @see getAttribute
1407     */
1408    public function __get($name)
1409    {
1410      if ($name=="options"){
1411        return json_encode( $this->config->options);
1412      }
1413      return parent::__get($name);
1414    }
1415}
1416