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